闲鱼业务代码解耦利器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开源准备工作仍然任重道远,可能短时间内无法与大家见面,但是大家可以参考本文来自己进行实现。如果文章发出后,大家有疑问,我们也会根据大家的疑问继续写相应的文章解答。

相关文章
|
存储 数据采集 监控
SkyWalking全景解析:从原理到实现的分布式追踪之旅
SkyWalking全景解析:从原理到实现的分布式追踪之旅
1941 1
|
运维 监控 算法
稳定性保障6步走:高可用系统大促作战指南!
年年有大促,大家对于大促稳定性保障这个词都不陌生,业务场景尽管各不相同,“套路”往往殊路同归,全链路压测、容量评估、限流、紧急预案等,来来去去总少不了那么几板斧。跳出这些“套路”,回到问题的本质,我们为什么要按照这些策略来做?除了口口相传的历史经验,我们还能做些什么?又有什么理论依据?
稳定性保障6步走:高可用系统大促作战指南!
|
安全 Java Linux
docker阿里云镜像加速
我们都知道因为某些原因我们访问外网都是比较慢的,比如我们使用maven下载依赖时是一个道理,同样的使用docker从docker.hub上下载镜像也是比较慢的。针对这种访问官网比较慢的情况有两种方案,第一种就是使用国内的仓库,第二种就是使用一个加速器。这里我们配置docker的镜像加速从来来实现提速。
13263 1
docker阿里云镜像加速
|
11月前
|
人工智能 JavaScript 数据可视化
Cursor、v0 和 Bolt.new:当今 AI 编程工具的全面解析与对比
本文深入解析了 Cursor AI、v0 和 Bolt.new 三大 AI 编程工具的特点与应用场景。Cursor 适合日常编码与团队协作,v0 专注于 UI 原型设计,Bolt.new 擅长全栈原型开发。各工具在功能上互为补充,开发者可根据需求灵活选择,以提升工作效率。
4843 1
|
12月前
|
Unix 数据安全/隐私保护
Mac电脑如何启用root用户
Mac电脑如何启用root用户
369 0
|
Go 开发工具 Python
【开发工具】Goland 2022.4 破解(by ja-netfilter)
【开发工具】Goland 2022.4 破解(by ja-netfilter)
854 1
【开发工具】Goland 2022.4 破解(by ja-netfilter)
|
人工智能 算法 Cloud Native
向量数据库小白必关注的“扫盲贴”来啦
自从ChatGPT问世以来,大语言模型受到广泛关注。但大模型更新频率低且不擅长垂直领域知识,向量数据库恰好可以与之互补,通过Retrieval Plugin为大模型和企业私有数据构建了一座桥梁,成为数据面向大模型的切入口。不过,你真的了解向量吗?什么又是向量数据库呢?向量检索的原理又是什么呢?向量爱好者们速速上车,跟着小编一起开启“向量探索之旅”吧!
4437 4
向量数据库小白必关注的“扫盲贴”来啦
|
存储 分布式计算 大数据
【云计算与大数据技术】分布式计算、虚拟化技术、并行编程技术等技术讲解(超详细必看)
【云计算与大数据技术】分布式计算、虚拟化技术、并行编程技术等技术讲解(超详细必看)
1299 1