閒魚業務代碼解耦利器SWAK是如何實現的(內含大量代碼)

做者:閒魚技術——塵蕭markdown

三年前,咱們發表了一篇文章給你們介紹了業務代碼解構利器SWAK,SWAK是Swiss Army Knife的簡稱,衆所周知,瑞士軍刀是一款小巧靈活、適用於多種場景的工具。在閒魚服務端,SWAK框架也是這樣一種小巧靈活、適用於多種場景的技術框架, 它能夠用於解決平臺代碼和業務代碼耦合嚴重難以分離;業務和業務之間代碼交織缺乏拆解的問題。以前咱們將其應用在了商品域的業務解耦上,如今咱們在搜索的膠水層中再一次遇到了這樣的問題,因此咱們決定將SWAK從新再拿出來讓其再放光彩。session

SWAK在閒魚搜索上的應用架構

目前閒魚搜索採用了新的端雲一體開發模式,引入了膠水層把客戶端的一部分邏輯移動到了服務端,以此來提高端側的動態能力,下降跟版本需求的數量,加快需求的上線速度。在運行這個新模式的過程當中,一開始膠水層部分的代碼結構設計較爲簡單,致使膠水層的代碼出現嚴重的耦合以及if-else邏輯膨脹的狀況。 undefined框架

咱們基於對業務的預判,進行了膠水層的重構,主要經過SWAK解決了兩個問題:ide

因爲入搜的垂直業務不斷增長,現有結構沒法支撐業務進行自定義,因此經過SWAK進行了一層解耦,根據不一樣的bizType(業務類型)索引到相應的頁面編排邏輯。保障了不一樣業務的頁面編排邏輯之間不會相互影響,小業務能夠複用商品搜索的頁面邏輯,也能夠本身進行定製。工具

卡片種類愈來愈多,致使了卡片解析器部分的if-else代碼膨脹。因此咱們引入了第二層SWAK,將卡片解析器部分的if-else去掉,改成經過cardType(卡片類型)索引到相應解析器的模式,避免後續的同窗在一堆if-else中暈頭轉向,找不到真正的邏輯。post

undefined性能

搜索膠水層目前還在總體的架構升級中,這邊先給你們簡單介紹一下,後續等架構升級完成後再寫文章給你們介紹一下閒魚搜索端雲一體的研發模式,本文先着重介紹一下SWAK的實現原理。優化

回顧SWAK使用方式ui

在講解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 { @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的實例。

這短短几句話中可能會產生幾個疑問,咱們一個一個來解答:

如何找到@SwakInterface標註的Bean 如何在Spring進行Bean註冊的時候進行偷樑換柱 代理類怎麼實現動態進行接口實現的替換 如何找到@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()方法了。

一樣是短短的兩句話,那麼有的小朋友可能會問了:

怎麼在@SwakSessionAop標記的方法體執行前插入咱們解析的代碼呢 解析出對應的實現類後,是怎麼"賦值"給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開源準備工做仍然任重道遠,可能短期內沒法與你們見面,可是你們能夠參考本文來本身進行實現。若是文章發出後,你們有疑問,咱們也會根據你們的疑問繼續寫相應的文章解答。

相關文章
相關標籤/搜索