如何避免重复造轮子,打造一个属于自己的组件库?

导语

作为java开发人员,“面向切面”这个词并不新奇,“插件”也存在于目前绝大多数平台和工具产品中,那在58房产中我们是如何将其合并且实际运用到生产中的呢?

本文详细介绍了58房产中面向切面插件器设计与实现,我们一起来深入了解一下

名词解释

SCF: 58集团自研的一套RPC远程服务通信框架(Service Communication Framework),支持跨平台,具有高并发,高性能,高可靠性,并提供异步、多协议、事件驱动的中间层服务框架,与阿里的dubbo属于相同一类。

WF: WF是Web Framework的缩写,它是58集团自研的一款基于JAVA语言的MVC开发框架,主要应用于基于JAVA的Web项目。

插件器与插件仓库切入点

什么是面向切面的插件器

所谓的“面向切面的插件器”实际上就是使用面向切面的思想开发一套插件器,并提供一系列现成完整的插件仓库给使用者,并支持根据业务需要开发自定义插件并无侵入性地嵌入到插件器中。

专业一点,插件器是一个仅依赖于cglib和slf4j的轻量级aop框架,支持同步和异步模式。插件器附带scf和wf的对接api,可以直接在scf和wf中使用。

发展历程

1. 背景及设计初衷

之前一直负责租房、二手房、商业地产、小区、经纪人等四端(pc、m、app、小程序)的web层与底层的基础微服务层等较多分散业务,日常工作避免不了业务需求频繁迭代。

如此多的业务系统分布在了不同的子部门及更细化的开发小组中,其中大家都有一些共性的技术型需求,如接口监控、报警、容灾(灾备)、服务降级、过载保护等等,虽然架构部给我们提供了强大的服务管理平台,但更细致更自定义化的处理还是必不可少,基本每个系统都要有类似的强需求。

一次我们做新项目时想要复用这些功能,发现所有这些都会对现有系统或多或少会有部分侵入性,还要稍加改造,大部分情况都存在“水土不服”的情况,为我们的项目帮助没有预想的那么大,因此我就想,有没有可能做到一次开发到处使用呢,从那以后就开始了“插件器”的初步设想与实验开发。

2. 原始方式为何不满足业务需求

原始过滤器实现的两种切入功能

首先就是要分析痛点,看看原有方式为何给我们造成了困惑,或为什么没有达到我们预想的那种效果呢,多方了解沟通,总结下来有以下几点:

a) 移植难&侵入性强:针对现有系统开发,大部分参数都写死在代码中;接入过程中,需要很多额外的理解与开发量,没办法做到无脑接入;

b) 无法满足所有框架:往往支持web框架的无法支持微服务框架,支持了微服务框架的又不支持内部方法的直接调用;

c) 影响性能:接入过多此类需求,很多都是串行累加,时间久了对核心的业务功能会造成性能上的大幅降低,最终演变成了系统重构;

原理与设计

1. 设计理念与痛点解决

a) 首先,定义“插件器”和“插件仓库”两个概念,绝对不能将两个东西混为一谈或设计开发过程中揉到一起。

b) 其次,采用面向切面的方式包装业务模块,通俗一点就是给需要处理的代码层加上代理。主要包装的目标为wf工程、scf工程、普通类调用等。

c) 再次,开发插件时直接继承标准插件抽象类,使得大家在开发插件时无需关注其未来的运行框架与位置,保证独立可运行即可。

d) 插件的配置采用注解方式,实际调研标识,这种方式可大大降低开发者的接入与使用成本。

e) 支持到参数级别,除类和方法外,最小粒度可作用于方法中的某个参数,

f) 采用异步扫描预加载+本地缓存方式,保证插件器的接入对原有工程启动及代理方法调用时的影响降低到最低(实际测试后证明其影响小到可忽略不计)。

g) 插件器尽量少的依赖外部开源jar包(只依赖cglib和slf4j),避免出现包冲突导致系统接入时遇到各种问题。

2. 功能对比

框架类型 优势 劣势
spring-aop 功能强大

1.太重,依赖很多jar

2.配置比较繁琐

aspectj

1.功能强大

2.依赖少

1.编译期植入

2.额外DSL学习成本

插件器

1.依赖少,仅依赖cglib和slf4j

2.使用简单

3.整合scf和wf

功能没有spring-aop和aspectj强大

与市面AOP组件优劣对比

3. 实现原理

插件器整体上可以分为如下几个模块(附模块图):

a) 业务类加载模块:该模块目的就是扫描所需业务的所有类文件,将标记为需支持插件的类及方法放到内存map中,也就是为了在代理层调用到该方法时可以实时从内存中找到该标记并切入适当的插件功能。主要为公共代理层做基本物料的准备。

b) 插件仓库加载模块:插件器的主要功能执行者,通过注解实现,便于维护及运行时加载。采用了“规约大于配置”的思想,只要是实现了指定接口的注解就将其注入到内存插件仓库中。同样也是为了公共代理层做基本物料的准备。

c) 对目标框架的切入模块:其实插件器及插件都缺少一个核心点,就是如何无缝切入到现有业务框架中,而不同的项目工程使用的业务框架也都不同,比如scf、wf、dubbo、spring等,需要针对不同的目标框架采用不同的切入方案。只有切入成功了才能让公共代理层发挥其真正的作用。而当需要接入一个新框架时,只需要开发针对这个框架的“切入器”,而其他模块完全都不需要改动,实现了模块间的解耦。

d) 公共代理层模块:该模块是插件器的核心模块,包括同步、异步的处理,插件与插件之前的上下文数据流转等。其本质也比较简单,就是通过反射的方式实现业务类的动态代理,将需要运行插件的执行前、执行后整体包装切入到业务方法的正确位置。

e) 异步线程池模块:因为考虑到不同插件的特点,有些插件(如日志收集)无需跟着业务方法实时运行的时候,可以考虑将插件的具体执行放到独立的线程池中,哪怕10秒之后运行完该功能也不影响,但业务方法不会因为耗时10秒的插件而导致超时。所以维护好该线程池也就有很大的必要性了。

f) 插件仓库模块:所有插件开发、提交、维护的一个虚拟仓库,每个插件的本质可以理解为”实现了指定接口的注解及注解实现类”,由于需要支持同步和异步两种插件执行方式,因此在定义开发某个新插件的时候,就要根据不同的定位来实现不同的接口,即当需要串行执行时,需实现一个IFPluginAnnotationSerial接口即可,而当插件器加载扫描到该插件的时候就知道该用什么方式来运行它了。

插件器模块划分

插件器调用时序

在各个框架下的处理流程

4. 效果展示

一个在方法前后打印日志的demo插件

在某方法上增加插件注解

运行效果

5. 优势分析

g) 一次性开发与接入,所有插件按标准开发后适用于所有框架,扩展插件依赖注解配置即可

h) 功能解耦,各个插件之间互相不依赖,对业务系统侵入性近乎为0

i) 支持同步+异步双模式,针对不同插件的功能特点,可以在开发插件之初就决定使用同步还是异步执行的方式来运行插件功能。

j) 自定义插件开发,除插件库中的公共插件可以随便供大家使用外,使用者还可随时开发专属于自己业务的插件放到自己工程中,插件器同样可以自动加载并运行。

k) 最重要的一点,用一行代码来代替之前一两天的工作量,在开发效率和快速迭代上会让人屡试不爽。

性能及影响

使用jmh工具,对插件器进行基准性能测试,测试模式为吞吐量。 基本总结如下:

1. 空方法性能测试

测试目标:测试aop代理类对比原生类,对性能的影响

测试结果:对于毫秒级的方法调用,插件器不会影响主业务的吞吐量

测试数据:

   空方法运行性能测试结果

测试结果说明:

a) milli 参数是模拟主方法实际执行时间,单位毫秒

b) baseline 方法是实例化Demo类,并调用baseline方法

c) noop 方法是使用插件器生成Demo类的代理,并调用noop方法,NoopAnno 插件是异步的空调用

d) noopSerialize 方法是使用插件器生成Demo类的代理,并调用noopSerialize方法,NoopAnnoSerialize 插件是同步的空调用

2. 同步异步性能测试

测试目标:测试插件器在同步和异步模式下,对性能的影响

测试结果:对于毫秒级的方法调用,插件器在异步模式下会提高吞吐量,在同步模式下和原生方法有一样的吞吐量。before + after 部分执行时间占总执行时间比例越大,异步模式对吞吐量的提升越大。

测试数据:

同步异步性能测试结果

测试结果说明:

a) consumingTime 参数是模拟各阶段执行时间,单位毫秒。10_20_10 代表 before_invoke_after,即before执行时间10毫秒,invoke执行时间20毫秒,after执行时间10毫秒。

b) baseline 方法是实例化Demo2类,并调用baseline方法

c) test 方法是使用插件器生成Demo2类的代理,并调用test方法,TestAnno 插件是异步调用

d) testSerialize 方法是使用插件器生成Demo2类的代理,并调用testSerialize方法,TestAnnoSerialize 插件是同步调用

落地与实践

1. 适用场景

a) 业务性较强的web项目,在入口位置非常适用。

b) 微服务项目,同web项目一样,可在入口位置使用。

c) 所有项目的内部方法直接调用,可细化到普通的get、set方法都可以使用该模式。

2. 现有插件仓库示例(按类别划分)

a) 监控(报警)相关

b) 缓存相关

c) 服务动态降级、熔断相关

d) 动态容灾(灾备)相关

e) 基本参数校验相关

f) 日志打印与收集相关

3. 自定义插件扩展与开发

插件仓库里的插件不够用怎么办?功能不完全符合自己要求怎么办?需要处理特定业务逻辑怎么办?

当然没问题!自定义插件扩展完全能够搞定,只需按照文档规范,分分钟即可开发属于自己的专属插件,且可单独放到自己的业务工程中,其他什么都不用做,工程重启后可自动加载运行,可谓方便之极。如过滤个敏感词、填充个额外信息、增加个权限校验、补充个自己喜欢的监控报警等等,都可以自己来实现。

下面是我们快速演示如何实现一个自己的插件:

a) 编写注解,DefinedPluginImpl引用的类为该注解的实现类,即第二步要编写的内容

插件注解类

b) 编写注解,DefinedPluginImpl引用的类为该注解的实现类,即第二步要编写的内容编写实现类, 需要继承 FPluginAnnotationAbstract,插件默认是以异步方式执行,如果需要以同步方式执行(例如在插件中返回数据等),需要继承IFPluginAnnotationSerial接口。

异步插件注解实现类

同步插件注解实现类

只需要一个注解外加一个注解实现,就可以随时生成自己想要的自定义插件,且无需任何配置,部署、重启、运行,接下来就能得到自己想要的结果了。

总结展望与规划

由于集团内部绝大部分使用的都是内部Framework,因此目前插件器还未支持到spring和dubbo等外部 框架中,但核心机制不变,只需要利用spring、dubbo原生的aop把插件器切入进去,那就顺利地把插件器合并到想要的Framework中了。 这也是接下来我们要继续做下去的版本。

另外,插件器的核心实际上是插件仓库,这个仓库需要对内“开源”,即所有人都可以发布上传,让其他人了解和使用自己的插件,随着贡献者的增加,我们在做一个业务项目时,可以在这个仓库里自己随意选择需要的模块进行组装,我们要把更多的精力放到主营业务中去。

相关简介

最后,给大家简单介绍下我们部门目前的职责与工作内容:

a) 参与58同城房产基础服务的技术架构设计,研发及维护工作;

b) 负责核心代码的编写工作并能够按时高质量交付;

c) 主导产品或项目中的关键技术问题攻关;

d) 技术分享并帮助其他成员提高解决技术问题的能力。

live

沙龙活动直播

2020年58技术沙龙活动在线直播第一弹——《大数据平台建设实践与探讨》已准备就绪,欢迎你强势围观!

详情:mag_right:请戳:point_up_2:图片查看, 2月 22日 本周六1 9:00 ,我们不见不散

阅读推荐

1. 独家|微服务网关组件在金融的实践

2. 基于有限状态机的广告状态管理方案及实现

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章