背景
一个商业应用如果既要提高稳定性,又要提高扩展性,而且有增值空间,插件系统是必不可少。如:JIRA,discuz等
通过提供插件开发,可以让其他人快速增强应用的同时又不会破坏基本功能。
而且可以根据不同用户开发不同的插件来进行客制化,提高盈利能力。
所以对于缺少AOP的Go语言,如何来实现这一目的呢?
接下来通过在设计管理工具中的实践来介绍一种简易的实现方式。
方案
实现插件其实最好的方式应该是通过AOP/注解,这样几乎可以实现无侵入。但是由于Golang缺少类似特性,所以需要考虑新的方案。
根据AOP的原理:在代码中插入一个锚点,然后通过锚点把上下文数据传给插件逻辑,再在插件逻辑中修改上下文中的数据后返回锚点的位置,接着执行。
所以,我们需要解决以下几个问题:
如何定义锚点
如何把相关数据传给锚点
如何返回数据到上下文
为了解决这几个问题,设计管理工具中是这样做的:
定义锚点
每一个锚点应该有一个名称,且因为有明确的作用,所以接收什么数据,返回什么数据应该是已经定义好的。而这个正是一个interface的特点,可以定义传入的参数和返回的数据,同时可以在一个命名下有多个动作。
同时一个interface下的多个方法,可以被组合成一套在不同地方需要同时的锚点,这样完美契合要求。如,设计管理工具中的路由锚点:

这里定义了2个锚点,都属于路由范畴,只是一个不使用应用原有的中间件,一个会使用,这样使得插件可以自己提供API接口。传递数据
有了定义,我们就只需要在指定地方把调用实现了这个接口的方法,同时把对应的参数传进去,有点类似于php中的call_user_func,比如:
由于有多个插件都可以实现同一个接口,所以此处我们给interface启用了一个别名route,避免和interface名称冲突。所以第一个参数,就变成"route.RegisterGroup",用于提示插件系统使用RouteProvider的RegisterGroup,然后将后面的数据依次传入即可。在方法的内部,我们需要确定指定的interface确实有这个方法,同时把由于为了能够接收任意参数,ApplyAnchor方法只能设定为interface{},所以在传入前,还需要使用反射将interface转成方法中需要使用的类型,如:

返回数据
返回数据会比较麻烦,虽然每个方法的返回数据类型已经固定,但是由于会有多个插件使用同一个接口,所以不同插件的返回数据直接如何合并则需要由上层决定,因此需要如实的放回全部的数据。如:
由于Golang的方法可以返回多个值,所以返回时,每个插件的返回数据都是一个slice,然后不同插件的slice再次组成slice,如果其中有错误的话,还需要单独识别。所以上层在使用时,需要“剥洋葱”式的转化类型来获取自己想要的数据,这个是目前无法避免的。如:
加载
完成了核心逻辑,之后需要来解决加载问题了。理想的插件系统开启和关闭应该不需要重启应用的,这样才能提供插件市场来实现盈利增长点。但是由于当下设计管理工具暂时还没有这个需求,所以使用了简单的方式来实现插件的加载和关闭,就是在启动时,使用–plugin参数来确定使用哪些插件。
这种方式不那么灵活,但是对于开发时间有限的情况下简单有效,对于不同的定制化客户来说,这种方式也是可以接受的。总结
这里分享了一种插件系统的实现方式以及实战中的使用,虽然只是一种简单的方式,但是还是实现了初步的业务逻辑与通用逻辑的解耦,并且为后续商用的定制化与扩展的打下了基础。