闲鱼业务代码解耦利器SWAK是如何实现的(内含大量代码)

简介: swak快刀斩乱麻

作者:闲鱼技术——尘萧

三年前,我们发表了一篇文章给大家介绍了业务代码解构利器SWAK,SWAK是Swiss Army Knife的简称,众所周知,瑞士军刀是一款小巧灵活、适用于多种场景的工具。在闲鱼服务端,SWAK框架也是这样一种小巧灵活、适用于多种场景的技术框架, 它可以用于解决平台代码和业务代码耦合严重难以分离;业务和业务之间代码交织缺少拆解的问题。之前我们将其应用在了商品域的业务解耦上,现在我们在搜索的胶水层中再一次遇到了这样的问题,所以我们决定将SWAK重新再拿出来让其再放光彩。

SWAK在闲鱼搜索上的应用

目前闲鱼搜索采用了新的端云一体开发模式,引入了胶水层把客户端的一部分逻辑移动到了服务端,以此来提升端侧的动态能力,降低跟版本需求的数量,加快需求的上线速度。在运行这个新模式的过程中,一开始胶水层部分的代码结构设计较为简单,导致胶水层的代码出现严重的耦合以及if-else逻辑膨胀的情况。
undefined

我们基于对业务的预判,进行了胶水层的重构,主要通过SWAK解决了两个问题:

  • 由于入搜的垂直业务不断增加,现有结构无法支撑业务进行自定义,所以通过SWAK进行了一层解耦,根据不同的bizType(业务类型)索引到相应的页面编排逻辑。保障了不同业务的页面编排逻辑之间不会相互影响,小业务可以复用商品搜索的页面逻辑,也可以自己进行定制。
  • 卡片种类越来越多,导致了卡片解析器部分的if-else代码膨胀。所以我们引入了第二层SWAK,将卡片解析器部分的if-else去掉,改为通过cardType(卡片类型)索引到相应解析器的模式,避免后续的同学在一堆if-else中晕头转向,找不到真正的逻辑。

undefined

搜索胶水层目前还在整体的架构升级中,这边先给大家简单介绍一下,后续等架构升级完成后再写文章给大家介绍一下闲鱼搜索端云一体的研发模式,本文先着重介绍一下SWAK的实现原理。

回顾SWAK使用方式

在讲解SWAK的原理之前,我们先简单地回顾一下SWAK的使用方式,以便更好的理解它的原理。我们先看一下SWAK解决的是什么问题。举个例子当我们在进入搜索的时候,需要判断不同的搜索类型,并返回不同的页面编排,这时候就需要根据这个类型走到不同分支的代码了,如果代码耦合在一个地方,后续这个文件会变得越来越难以维护。

if(搜商品) {
    if(搜商品A版本) {
        doSomething1();
    }else if(搜商品B版本) {
        doSomething2();
    }
} else if(搜会玩) {
    doSomething3();
} else if(搜用户) {
    if(搜用户A版本) {
        doSomething4();
    }else if(搜用户B版本) {
        doSomething5();
    }
}

SWAK的出现,就是为了解决上文中的这种情况,我们可以将所有的if-else对应的逻辑平铺开来,将其变为TAG,通过SWAK进行路由。

/**
 * 1.首先先定义一个接口
 */
@SwakInterface(desc = "组件解析") // 使用注解声明这是一个多实现接口
public interface IPage {
    SearchPage parse(Object data);
}

/**
 * 2.然后编写相应的实现,这个实现可以有很多个,用TAG进行标识
 */
@Component
@SwakTag(tags = {ComponentTypeTag.COMMON_SEARCH})
public class CommonSearchPage implements IPage {
    @Override
    public SearchPage parse(Object data) {
        return null;
    }
}

/**
 * 3.编写Swak路由的入口
 */
@Component
public class PageService {
    @Autowired
    private IPage iPage;

    @SwakSessionAop(tagGroupParserClass = PageTypeParser.class,
        instanceClass = String.class)
    public SearchPage getPage(String pageType, Object data) {
        return iPage.parse(data);
    }
}

/**
 * 4.编写相应的解析类
 */
public class PageTypeParser implements SwakTagGroupParser<String> {
    @Override
    public SwakTagGroup parse(String pageType) {
            // pageType = ComponentTypeTag.COMMON_SEARCH
        return new SwakTagGroup.Builder().buildByTags(pageType);
    }
}

代码虽然没几行,但是覆盖了SWAK全部的核心流程,其中最核心的问题是【SWAK如何找到相对应的接口实现】,这个问题我们需要分为 注册过程执行过程 两个部分来解答

图片2

注册过程

因为闲鱼服务端应用基本都基于Spring框架,所以SWAK在设计的时候就借用了很多Spring的特性。Spring相关的特性如果有不了解的可以自行进行查阅,这边就不进行详细介绍了。

以上面的例子为例,注册阶段主要的目的是找到@SwakInterface标注的IPage类并将其交给Spring容器进行托管,这样在使用的时候可以天然使用到Spring的依赖注入能力。同时为了后续能动态进行接口实现的替换,我们不能直接把找到的类注册到Spring容器中,我们需要将其hook成一个代理类,并在代理类中根据情况返回不同@SwakTag的实例。

这短短几句话中可能会产生几个疑问,我们一个一个来解答:

  1. 如何找到@SwakInterface标注的Bean
  2. 如何在Spring进行Bean注册的时候进行偷梁换柱
  3. 代理类怎么实现动态进行接口实现的替换

如何找到@SwakInterface标注的Bean

在JAVA中通常我们获取自定义注解的方式是使用反射,所以这里需要做的就是扫描所有的类(这边可以自行优化一下扫描范围,可以只扫描特定路径下的类)并且通过反射获取自定义注解。扫描库的代码自己编写逻辑当然是可以的,但是使用开源框架也是一个很好的选择,这边给大家推荐一下ronmamo的reflections库(Github),库的实现原理这边就不详细介绍了,使用方法也很简单,直接上代码吧

    public Set<Class<?>> getSwakInterface() {
        Reflections reflections = new Reflections(new ConfigurationBuilder()
            .addUrls(ClasspathHelper.forPackage(this.packagePath))
            .setScanners(new TypeAnnotationsScanner(), new SubTypesScanner())
        );
        return reflections.getTypesAnnotatedWith(SwakInterface.class);
    }

除了扫描@SwakInterface之外,我们也应该把 @SwakTag对应的类也扫出来,并将其存在一个map中,保证我们后面可以通过Tag去找到一个Class。

如何在Spring进行Bean注册的时候进行偷梁换柱

这个口子其实Spring已经给我们都准备好了,Spring在Bean的注册阶段会获取容器中所有类型为BeanDefinitionRegistryPostProcessor的bean,并调用postProcessBeanDefinitionRegistry方法,所以我们可以直接继承这个类并重写相应的方法来Hook这一流程。在这个方法中,我们可以创建一个新的BeanDefinition并将准备好的代理类作为BeanClass设置进去,这样生成对应的Bean时,就会直接使用到我们准备好的代理类了。(这里的原理涉及到Spring Bean的注册过程,可以自行查阅资料,不再详述)

@Configuration
public class ProxyBeanDefinitionRegister implements BeanDefinitionRegistryPostProcessor {
  
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
        Set<Class> typesAnnotatedWith = getSwakInterface();

        for (Class superClass : typesAnnotatedWith) {
            if (!superClass.isInterface()) {
                continue;
            }

            RootBeanDefinition beanDefinition = new RootBeanDefinition();
            beanDefinition.setBeanClass(SwakInterfaceProxyFactoryBean.class);

            beanDefinition.getPropertyValues().addPropertyValue("swakInterfaceClass", superClass);
            String beanName = superClass.getName();
            beanDefinition.setPrimary(true);
            beanDefinitionRegistry.registerBeanDefinition(beanName, beanDefinition);
        }
    }
}

代理类怎么实现动态进行接口实现的替换

在上一步中,我们准备了一个SwakInterfaceProxyFactoryBean作为代理类注册到了BeanDefinitionMap中,但其实SwakInterfaceProxyFactoryBean严格意义上来说并不是一个代理类,正如它名字所描述的它是一个FactoryBean,FactoryBean是Spring中用来创建比较复杂的bean的一个类,在这个类的getObject()方法中,我们真正地使用动态代理的方式创建相应的对象,创建出相应的对象。

在动态代理方式的选择上,我们使用了 cglib 实现动态代理,因为JDK中自带的动态代理机制只能代理实现接口的类,而cglib可以为没有实现接口的类提供代理并且能够提供更好的性能。cglib的介绍网上有很多,这边就不详细介绍了。在Enhancer中设置一个CallBack,在这个被代理的类调用方法的时候,就会回调我们设置进去的SwakInterfaceProxy.intercept()方法进行拦截。intercept()方法我们放到下面执行过程中再进行详细介绍,先看看这部分代码

public class SwakInterfaceProxyFactoryBean implements FactoryBean {
    @Override
    public Object getObject() {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(this);
        this.clazz = clazz;
        // 这里一般不用new出来,可以把SwakInterfaceProxy也交给Spring进行托管,这里为了表述清晰用new指代一下
        enhancer.setCallback(new SwakInterfaceProxy());
        // 返回代理对象,返回的对象起始就是一个封装了"实体类"的代理类,是实现类的实例。
        return enhancer.create();
    }
}

执行过程

在执行过程中,我们的主要目标是在@SwakSessionAop标记的方法体执行前,通过SwakTagGroupParser 根据参数解析出成员变量IPage iPage对应的实现类CommonSearchPage,之后在这个方法体中调用ipage.parse()就会直接调用到CommonSearchPage.parse()方法了。

同样是短短的两句话,那么有的小朋友可能会问了:

  1. 怎么在@SwakSessionAop标记的方法体执行前插入我们解析的代码呢
  2. 解析出对应的实现类后,是怎么"赋值"给iPage变量的

如何在方法前面插入代码

看到这个问题自然而然地想到,这不就是要做一个AOP嘛,这玩意Spring都帮我们搞定了,Spring的AOP是通过cglib实现的基于JVM的动态代理,并做了一层很好用的封装。我们可以使用@Around注解在方法前进行一层切面来执行我们的代码,我们先使用SwakTagGroupParser解析tagGroup并将解析出来的tagGroup存起来,然后可以调用jointPoint.proceed()继续执行方法体,这样在方法体中所使用到的iPage就会使用到相应的实现了。

可能有的人会有疑问,这里不就是把tagGroup存了一下吗?这么后面就会使得iPage使用相应的实现呢?关于这个我们在下一个问题中进行描述。

@Component
@Aspect
public class SwakSessionInterceptor {

    @Pointcut("@annotation(com.taobao.idle.swak.core.aop.SwakSessionAop)")
    public void sessionAop() {
    }


    @Around("sessionAop()&&@annotation(swakSessionAop)")
    public Object execute(ProceedingJoinPoint jointPoint, SwakSessionAop swakSessionAop) {
          // 根据类型获取需要传入Parser的参数
        Class instanceClass = swakSessionAop.instanceClass();
          Object sessionInstance;
          for (Object object : args) {
            if (instanceClass.isAssignableFrom(object.getClass())) {
                sessionInstance = object;
            }
        }
      
          //通过Parser解析出相应的tagGroup
        Class parserClass = swakSessionAop.tagGroupParserClass();
        SwakTagGroupParser swakTagGroupParser = (SwakTagGroupParser)(parserClass.newInstance());
        SwakTagGroup tagGroup = swakTagGroupParser.parse(sessionInstance);
      
        try {
            //SwakSessionHolder就是一个储存tagGroup的地方,可以随意实现
            SwakSessionHolder.hold(tagGroup);
            Object object = jointPoint.proceed();
            return object;
        } finally {
            SwakSessionHolder.clear();
        }
    }
}

如何"赋值"iPage变量

首先我需要解释一下为什么一直在给"赋值"打引号,因为这部分确实不是真的去给iPage赋值,但是达到的效果是一样的。还记得之前我们把@SwakInterface所标注的类在注册的时候做了一层动态代理,所以iPage对应的对象在调用方法前,都会调用一下之前提到的intercept()方法,在这个方法中,我们可以通过之前存起来的tagGroup找到需要调用的SwakTag,并通过SwakTag找到相应的实现类的实例,最后通过method.invoke()方法调用其实例。

关于反射的相关API这里就不详细介绍了,引用一下 廖雪峰对于Method的解释:对 Method实例调用 invoke就相当于调用该方法, invoke的第一个参数是对象实例,即在哪个实例上调用该方法,后面的可变参数要与方法参数一致,否则将报错。
public class SwakInterfaceProxy implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] parameters, MethodProxy methodProxy) throws Throwable {
            String interfaceName = clazz.getName();
        SwakTagGroup tagGroup = SwakSessionHolder.getTagGroup();
        
        // 这里还可以根据tag的优先级配置调整执行顺序,这里就简单取一下
        List<String> tags = tagGroup.getTags();
        Object retResult = null;
        try {
            // 按照优先级依次执行
            for (String tag : tags) {
                // 根据TAG可以获取到实现类的实例
                // 可能第一次用,那么没有实例只有Class,拿Class去Spring容器里找对应的实例
                Object tagImplInstance = getInvokeInstance(tag);
                retResult = method.invoke(tagImplInstance, parameters);
            }
            return retResult;
        } catch (Throwable t) {
            throw t;
        }
    }
}

至此,一次完整的使用SWAK调用方法的流程就完成了。

总结

本文重点对SWAK的原理进行了阐述,同时贴上了部分关键代码实现,文中涉及的部分代码,为了减少理解成本和篇幅做了一定程度的删减,切忌直接拷贝使用。SWAK开源准备工作仍然任重道远,可能短时间内无法与大家见面,但是大家可以参考本文来自己进行实现。如果文章发出后,大家有疑问,我们也会根据大家的疑问继续写相应的文章解答。

相关文章
|
2月前
|
存储 供应链 安全
dapp系统开发详细规则/玩法功能/案例设计/源码步骤
DApp是指去中心化应用(Decentralized Application),是构建在区块链技术之上的应用程序。与传统的中心化应用不同,DApp不依赖于中心化的服务器或管理者,而是通过智能合约和分布式网络来实现去中心化的运行。
|
22天前
|
安全
什么是短剧系统开发/需求设计/逻辑方案/项目指南
The short drama system development plan refers to the development of a system for organizing and managing the process of short drama production, release, and playback.
|
27天前
|
算法 测试技术 数据处理
【C++ 设计思路】优化C++项目:高效解耦库接口的实战指南
【C++ 设计思路】优化C++项目:高效解耦库接口的实战指南
74 5
|
2月前
|
API Nacos
【想进大厂还不会阅读源码】ShenYu源码-重构同步数据服务
ShenYu源码阅读📚。我们看下PR的标题和Concersation的头一句,大概意思就是重构注册中心数据同步到ShenYu网关的方式。大家看看重构了有没好处呢?不仅获得了知识,还获得了一次开源贡献,何乐而不为呢
52 3
|
6月前
|
存储 开发框架 安全
dapp去中心化大小公排项目系统开发案例详情丨规则玩法丨需求逻辑丨方案项目丨源码程序
区块链技术的去中心化应用(DApp)开发在近年来逐渐受到广泛关注。大小公排互助系统是一种较为流行的DApp模式之一,其基本特点是参与者按照加入顺序依次排队,
|
6月前
|
敏捷开发 存储 测试技术
链动2+1系统开发项目案例丨指南教程丨需求方案丨功能设计丨成熟技术丨步骤逻辑丨源码程序
用户需求导向:系统开发应以用户需求为中心,从用户的角度思考,了解用户的真实需求和期望,以提供优质的用户体验。
|
8月前
|
存储 安全 区块链
区块链游戏系统开发(开发详细)/案例开发/设计功能/逻辑方案/源码平台
  区块链游戏系统开发是一个复杂而精密的过程。首先,需要进行需求分析和规划,确定游戏系统的功能和特性。然后,进行技术选型和架构设计,选择适合的区块链平台和开发工具。接下来,进行系统的搭建和编码,实现游戏逻辑和用户交互功能。最后,进行测试和优化,确保系统的稳定性和性能。
|
9月前
|
设计模式 安全 Java
基于设计模式改造短信网关服务实战篇(设计思想、方案呈现、源码)
基于设计模式改造短信网关服务实战篇(设计思想、方案呈现、源码)
210 0
|
10月前
|
架构师 程序员
化繁为简!阿里新产亿级流量系统设计核心原理高级笔记(终极版)
不管是初入职场的小菜鸟还是有一些工作年限的老司机,系统设计问题对他们来说都是一大困扰。前者主要是在于面试;面试官来一个如何从零到一设计一个完整的系统?大多数人都会直接懵了,因为系统设计覆盖面广,而网上资料又不能面面俱到,单独背背文章肯定是不行的;后者主要在于晋升;想要从程序员进阶到架构师,系统设计是必须要踏入的一道坎,他对你的技术广度跟深度都会有一定程度的考察。
|
11月前
|
供应链 前端开发 应用服务中间件
几种微前端方案探究
随着技术的发展,前端应用承载的内容也日益复杂,基于此而产生的各种问题也应运而生,从MPA(Multi-Page Application,多页应用)到SPA(Single-Page Application,单页应用),虽然解决了切换体验的延迟问题,但也带来了首次加载时间长,以及工程爆炸增长后带来的巨石应用(Monolithic)问题;对于MPA来说,其部署简单,各应用之间天然硬隔离,并且具备技术栈无关、独立开发、独立部署等特点。要是能够将这两方的特点结合起来,会不会给用户和开发带来更好的用户体验?至此,在借鉴了微服务理念下,微前端便应运而生。
185 0