閱讀554 返回首頁    go 阿裏雲 go 技術社區[雲棲]


平台化三部曲之一微核心可擴展架構 - 從Eclipse平台看交易平台化

該文章來自阿裏巴巴技術協會(ATA)精選集

從Eclipse平台看交易平台化

淘寶網的交易平台伴隨著互聯網,網絡購物的蓬勃發展,支持淘寶網成為全球最大的在線交易平台。各種業務方和他們新的交易類型對交易平台提出各種各樣的需求,讓交易係統的響應和業務支持在現有係統基礎上越來越顯露出其係統架構上的缺陷,架構缺乏平台化定製擴展的功能,在快速支持新業務,擴展業務功能方麵越發捉襟見肘,隻能通過加大開發團隊力量的投入來滿足業務方的需求。

最近交易開始"平台化",希望通過的業務模型,業務流程的重構,能夠快速支持業務發展。本文我們將討論平台化建設,以及就Eclipse這個平台化的典型,借鑒其中平台化的設計理念來探討交易平台化。

平台化的目標

平台的開發,一般包括兩方麵的內容,一個是可複用性,一個是可配置型。Eclipse作為成功的開發工具集成平台,這這兩方麵提供了很好的典範,我們可以借鑒Eclipse平台設計的理念.

Eclipse是一個怎樣的平台

Eclipse是一種可擴展的開放源代碼IDE。2001年11月,IBM公司捐出價值4,000萬美元的源代碼組建了Eclipse聯盟,並由該聯盟負責這種工具的後續開發。。集成開發環境(IDE)經常將其應用範圍限定在“開發、構建和調試”的周期之中。為了幫助集成開發環境(IDE)克服目前的局限性,業界廠商合作創建了Eclipse平台。

Eclipse允許在同一IDE中集成來自不同供應商的工具,並實現了工具之間的互操作性,從而顯著改變了項目工作流程,使開發者可以專注在實際的嵌入式目標上。Eclipse框架的這種靈活性來源於其擴展點。它們是在XML中定義的已知接口,並充當插件的耦合點。擴展點的範圍包括從用在常規表述過濾器中的簡單字符串,到一個Java類的描述。任何Eclipse插件定義的擴展點都能夠被其它插件使用,反之,任何Eclipse插件也可以遵從其它插件定義的擴展點。

除了解由擴展點定義的接口外,插件不知道它們通過擴展點提供的服務將如何被使用。利用Eclipse,我們可以將高級設計(也許是采用UML)與低級開發工具(如應用調試器等)結合在一起。如果這些互相補充的獨立工具采用Eclipse擴展點彼此連接。

Eclipse從設計的開始,就以可擴展為目標,為此Eclipse的架構師們設計了優美的平台基礎架構,從而保障開發工具開發者可以為Eclipse提供多種多樣的插件滿足各種開發功能的需要。而Eclipse後來的成功也證明了這種架構在擴展和定製方麵的強大生命力。

也就是說Eclipse是為了滿足多種業務需求(不同的開發工具,開發語言,從設計開始就做了IDE的平台化設計,可以為後續的不同開發工具需求提供快速的接入和實現機製。

那麼接著我們以平台化的視角看看Eclipse的平台化(“平台+插件”)有什麼獨到之處,以及我們可以學到什麼。

Eclipse平台核心功能

Eclipse采用“平台+插件”的體係結構,平台僅僅作為一個容器,所有的業務功能都封裝在插件中,通過插件組件構建開發環境。

Platform Runtime平台運行庫是內核,Eclipse所有的功能就是通過這個runtime和插件一起完成。 在這個runtime裏,定義Eclipse的核心功能。在下麵的章節,會介紹Eclipse Runtime擴展實現的機製。通過學習這些機製,我們可以學到以下經驗:

  1. Eclipse的核心類是怎樣做到保持穩定,同時又怎樣支持以後得各種功能擴展。
  2. Eclipse如何對多業務的支持和新業務的快速接入。

首先我們來認識一個非常重要的接口:

IAdapable

在Eclipse SDK版本下,用F4查看IAdaptable的類繼承關係,你會發現IAdaplabe接口使用的是多麼廣泛,基本上Eclipse的核心類都繼承這個接口。
IAdaptable_Hierarchy
如果你想了解Eclipse的插件開發或者他的擴展機製,那麼你需要深刻理解IAdaptable接口,這個接口是Eclipse的核心模式,是Eclipse擴展機製的核心,就是這個簡單的IAdaptable接口奠定了整個Eclipse擴展平台的基礎,對Eclipse開發者來說,這個接口就像Java的Exception,Object一樣,無處不在。

什麼是IAdaptable接口

直接看源代碼:

public interface IAdaptable {
/**
 * Returns an object which is an instance of the given class
 * associated with this object. Returns <code>null</code> if
 * no such object can be found.
 *
 * @param adapter the adapter class to look up
 * @return a object castable to the given class, 
 *    or <code>null</code> if this object does not
 *    have an adapter for the given class
 */
public Object getAdapter(Class adapter);  }

這個接口隻提供了唯一的一個方法getAdapter,從注釋中可以了解到,他提供了一種將現有接口適配到給定類的方式,通過這種方式,從而對現有類的功能進行了擴展。

為什麼需要這個接口,它有什麼特別之處

當我們要增加一些新特性時,這些新特性可能需要用到已有接口提供的特性,那我們要怎樣在新接口中使用這些特性。

我們通過一個Eclipse裏的實例來說明IAdaptable的特別之處。Eclipse的Properteis View用來顯示選中對象的Properteis。在Project View中,如果你選中一個項目,一個文件,選中對象的屬性都會顯示在Properties View中。那麼這是如何實現的。

Properties View需要一個接口獲取對象的屬性並顯示他們,在Eclipse中,這個接口是IPropertySource。 Eclipse的Resource 插件提供了IFile接口,用來代表Project View中對應的文件。如果按照常用的做法:讓新接口繼承已有接口。

public interface IFile extends IPropertySource

這種方式有兩個缺陷:

  1. IFIle實現中必須增加IPropertySource的實現,意味著這對核心類的修改
  2. 如果還需要實現其他更多的接口,這個方式會導致接口的膨脹。

如果IFile通過實現IAdaptable接口的方式來實現對IPropertySouce的支持,如下所示:

public Object getAdapter(Class adapter){
    if(adapter.equals(IPropertySource.class)){
        return new PropertySourceAdapter(this);
    }
    return null;
}

PropertySouceAdapter:

class PropertySourceAdapter implements IPropertySource{ 
    private final Object item;

    public PropertySourceAdapter(Object item){
        this.item = item;
    }
    //required methods of IPropertySource ...
}

可以看到,這種方式可以靈活的實現對特定接口的支持。在上麵代碼中,IFile實現也被修改,但在Eclipse真正的實現中,IFile實現時不需要修改的,Adapter實現類通過在plugin.xml中配置完成。

<plugin>
 <extension
     point="org.eclipse.core.runtime.adapters">
  <factory
        adaptableType="org.eclipse.core.resources.IFile"
        class="org.eclipse.core.adapters.properties.FilePropertiesSourceAdapterFactory">
     <adapter
           type="org.eclipse.ui.views.properties.IPropertySource">
     </adapter>
  </factory>
 </extension>
</plugin>

通過這種方式,我們發現這些核心類的實現在保持不變的情況下,可以通過IAdaptable接口以及Eclipse平台提供的注冊機製,能夠優雅的將現有類擴展支持其他的接口。

這種方式對於平台化來說十分重要,

首先,保持核心類的穩定,避免因為新功能的加入,需要對核心實現進行修改,這很容易導業務模型因為功能的增加和膨脹,不時的修改核心代碼也很容易降低平台的穩定性。

其次, 新功能的引入和已有功能實現了隔離,他們在各自的實現類中完成功能邏輯,方便維護和測試

IAdapatable對交易平台的思考

Eclipse的核心代碼設計非常簡潔優美,發展10多年來,核心代碼變動不大,但Eclipse平台支持的功能在Eclipse開源社區和各廠家的支持下,成倍的增加。 能夠讓各種工具在這些核心代碼的基礎上實現各自的功能,確不需要更改核心代碼,相互之間又有很好的隔離性,這些核心代碼的質量和背後的設計思想的強大能力讓人讚歎。

目前交易平台的核心代碼需要學習Eclipse的這一點,**對核心接口和實現應該達到不因為新業務新功能的引入就需要更改接口的目標。** 這樣保持核心接口穩定,避免代碼膨脹,對平台的生命力有很大的好處。

舉例來說,下麵代碼是Buy裏商品的基本接口

public interface ItemSDO extends ExtensibleSDO {
  ....   
  boolean isHotel();

  boolean isInsurance();

  boolean isOfferItemNoPaid();

  boolean isQrCodeSend();
  ....  }

有大量類似的接口方法,用來判斷商品的業務類型,也就是說,如果需要增加一個新的業務,那麼這個接口就會多一個方法。對這樣核心的類庫果各個業務開發團隊都可以去修改,其風險以及以後得代碼維護性都可想而知。

這樣的例子在交易的代碼中很普遍,我們需要在平台化中去改進這一點,設計核心代碼可以滿足以後業務的發展需要,讓業務開發團隊沒必要去修改這些核心代碼。

再比如, 打標這個操作可以說是交易中最常見的業務能力。達標就是對訂單或者商品加上新的屬性值。這其實很類似Eclipse的IFile和IPropertySource的功能。在Eclipse的Properties View中,你可以對選中文件的一些屬性進行修改或者增加屬性。

建設訂單接口設計好後,需要增加達標的能力,並且把標記通過查詢接口顯示出來。 如果按照Eclipse的方式, 訂單,商品都可以先繼承IAdaptable接口:

public interface ItemSDO extends IAdaptable {
 ...
}
public interface OrderLineSDO extends IAdaptable {
 ...
}

而打標相應的接口封裝在一個獨立的類中:(直接用Eclipse的IProertySource做參考,和打標的設計目的基本一致:

public interface IPropertySource {
   ...
   public void setPropertyValue(Object id, Object value);
   ...
}

...

如果商品和訂單需要打標的能力,那麼隻需要簡單配置下:

<plugin>
 <extension
     point="org.eclipse.core.runtime.adapters">
  <factory
        adaptableType="org.eclipse.core.resources.ItemSDO"
        class="org.taobao.core.adapters.properties.ItemSDOPropertiesSourceAdapterFactory">
     <adapter
           type="com.taobao.buy.properties.IPropertySource">
     </adapter>
  </factory>
 </extension>
</plugin>

生成一個新的類: ItemSDOPropertiesSourceAdapterFactory在這個類中去實現對商品的打標擴展:

public class ItemSDOPropertiesSourceAdapterFactory implements IAdapterFactory {
  public Object getAdapter(Object adaptableObject, Class adapterType) {
    if (adapterType == IPropertySource.class)
        return new ItemSDOPropertySource((ItemSDO)adaptableObject);
    return null;
  }

  public Class[] getAdapterList() {
    return new Class[] {IPropertySource.class};
  }
}

可以看到,增加了打標這個能力後,商品或者訂單接口都不需要修改。而且打標這個能力被抽象出來,可以被訂單,商品等等擴展。

交易平台可以說是目前阿裏中設計業務方最多的平台,B2B,航旅,O2O,虛擬商品,生活服務等新的業務快速增加,對業務的支持能力是交易平台化最核心和頭痛的問題。目前交易的代碼裏到處是和業務相關的代碼。

對比Eclipse,可以說Eclipse也是支持很多業務方的平台,在Eclipse的集成開發環境裏,可以支持Java,Web,Scala等各種功能工具。 那麼Eclipse是怎樣解決業務隔離和快速支持新業務呢,我們可以做個類比, 交易平台需要創建各種不同業務類型的訂單,如航旅訂單,生活服務訂單,酒店訂單。 Eclipse需要創建不同業務類型的項目,比如Java Project,Maven Project, Scala Project, J2EE Project。 每種項目就和訂單一樣,有各自不同的特性和對應的行為能力。 
我們首先介紹Eclipse的項目模型以及如果對項目支持不同的業務擴展,然後我們在對比下目前交易平台TMF的實現,看看交易平台化對業務支持可以從Eclipse中學到什麼。

Project項目是Eclipse中最基本的組織單元。 使用Eclipse做開發,都是從創建一個項目開始,在Eclipse中,點擊File->New Project...你可以發現Eclipse支持各種各樣的Project類型,如下圖所示:

選擇不同的project,你會發先創建項目的流程,項目的特性都會有所不同。比如選擇創建Maven Project,在項目創建中,會提示你設置項目的Maven artifact信息,項目生成後, 項目中會創建POM.xml文件,鼠標選中項目,右鍵菜單會出現Maven相關菜單。 這一切背後是怎麼實現的?

在Eclipse裏,項目一般是IProject的實例,我們先簡單創建一個最基本的Eclipse Project,File-> New General Project:
項目創建後,隻有一個文件.project, 這個文件時eclipse項目的元數據,打開這個文件:

<?xml version="1.0" encoding="UTF-8"?>
  <projectDescription>
  <name>Test</name>
  <comment></comment>
  <projects>
  </projects>
  <buildSpec>
  </buildSpec>
  <natures>
  </natures>
</projectDescription>

鼠標選中項目,右鍵菜單中隻有最基本的操作,在Run As中也隻有Run Configruation這一個子菜單。

現在我們在這個文件中的natures部分增加:

<natures>
    <nature>org.eclipse.jdt.core.javanature</nature>
</natures>

這時,鼠標右鍵Run As中,會出現Java Application, Java Applet兩個新的子菜單。 這個新增加的Nature是項目的某種標記tag,增加org.eclipse.jdt.core.javanature這個nature會告訴Eclipse這是個Java項目。 同理我們可以增加代表maven項目的標記, org.eclipse.m2e.core.maven2Nature,那麼這個項目就不但是個java projet,同時也是一個maven project。

<natures>
    <nature>org.eclipse.jdt.core.javanature</nature>
    <nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>

這個簡單的測試,告訴我們Eclipse Project的特定類型是由project nature來標識的。 Eclipse的功能室通過插件方式來實現的,比如 Java Development Tool (JDT) 插件提供了java相關功能,JDT會聲明我識別具有“org.eclipse.jdt.core.javanature”的項目為Java項目,所有java相關的功能就通過這個nature在插件和項目之間建立起聯係。我們可以在跟具體了解下Eclipse對應的實現,主要和以下三個接口相關:

IProject, IProjectDescription, IProjectNature

首先我們看看IProjet的部分接口:

public interface IProject extends IContainer, IAdaptable {
  ...
 /** 
   * Returns whether the project nature specified by the given
   * nature extension id has been added to this project. 
   *
   * @param natureId the nature extension identifier
   * @return <code>true</code> if the project has the given nature 
   * @exception CoreException if this method fails. Reasons include:
   * <ul>
   * <li> This project does not exist.</li>
   * <li> This project is not open.</li>
   * </ul>
 */
  public boolean hasNature(String natureId) throws CoreException;
  public boolean isNatureEnabled(String natureId) throws CoreException;
  public IProjectNature getNature(String natureId) throws CoreException;
  public IProjectDescription getDescription() throws CoreException;
  public void setDescription(IProjectDescription description, IProgressMonitor monitor) throws CoreException;
...
}

IProjectDescription:

public interface IProjectDescription {
   ...
   public void setNatureIds(String[] natures);
   public String[] getNatureIds();
   public boolean hasNature(String natureId);
   ...
}

IProjectNature:

public interface IProjectNature {
/** 
 * Configures this nature for its project. This is called by the workspace 
 * when natures are added to the project using <code>IProject.setDescription</code>
 * and should not be called directly by clients.  The nature extension 
 * id is added to the list of natures before this method is called,
 * and need not be added here.
 * 
 * Exceptions thrown by this method will be propagated back to the caller
 * of <code>IProject.setDescription</code>, but the nature will remain in
 * the project description.
 *
 * @exception CoreException if this method fails.
 */
public void configure() throws CoreException;

/** 
 * De-configures this nature for its project.  This is called by the workspace 
 * when natures are removed from the project using 
 * <code>IProject.setDescription</code> and should not be called directly by 
 * clients.  The nature extension id is removed from the list of natures before 
 * this method is called, and need not be removed here.
 * 
 * Exceptions thrown by this method will be propagated back to the caller
 * of <code>IProject.setDescription</code>, but the nature will still be 
 * removed from the project description.
 * *
 * @exception CoreException if this method fails. 
 */
public void deconfigure() throws CoreException;

從以上代碼可知, 當一個Project Nature被賦予一個項目之後, 那麼這個Nature的Configure方法就會被調用,這這個方法裏,可以把和這個Nature相關的業務邏輯增加到項目中去,比如Java Nature被增加後,Configure方法會把Java的builder注入到項目裏,當運行項目build時,java 的builder就會被執行。

簡單總結一下:

  1. Project Nature允許插件(業務方)把項目project標記tag為一種特定類型(業務), 這個Nature由業務方定義聲明,通過這個Nature標記,可以把業務方定義的特定功能增加到project中去。
  2. 每個項目可以有多個nature,來自不同的業務方,可以說nature是業務方的一個標記。 通過這個標記,可以對項目的行為根據業務方的要求來定製。

我們可以到這是一種優美的對多業務的支持,業務能力通過一個標記來聲明, 模型隻需要簡單的增加標記,就可以得到對應的業務能力,業務和模型之間通過標記解耦開來。 無論增加多少業務,都不會影響模型的實現,各業務之間也是隔離的。

如果有類似的方式對訂單實現多業務擴展, 那麼如果新增生活服務訂單,隻需要生活服務定義一個新的nature標記, 生活服務相關的行為能力隻和這個nature關聯。 當一個訂單被生成識別成生活服務訂單後,訂單中會加入生活服務nature標記, 這個訂單就具有所有和生活相關服務的能力了。

在稍微了解Eclipse對多業務的支持後,我們來了解下目前交易對訂單多業務支持的實現, 目前對多業務是由TMF2.0重構來實現,我們用個實例來說明TMF的實現:
在Orderline.java(訂單)中,

@Override
public boolean canUseMallPoint() {
    CanUseMallPoint provider = Providers.getProvider(CanUseMallPoint.class);
    //返回值一定包含一個值,所以此處不用判斷provider的返回值是否為null
    return provider.canUseMallPoint(this);
}

這個方法用來判斷該訂單是否可以使用天貓積分。 
在這個實現裏, 通過TMF的Provider和SPI的方式,根據業務方來決定是否可以使用天貓積分:
在對應的provider實現中: 
public class CanUseMallPointProvider extends SpiProvider2
implements CanUseMallPoint {
@Override
public Boolean canUseMallPoint(OrderLineSDO orderLineSDO) {
return null;
}

  @Nonnull
  @Override
  protected SpiNode<UnitedRequest, UnitedResponse> spiTree() {
    return mutex(NamespaceConstants.GuestNS,
            NamespaceConstants.DefaultFictitiousNs,
            NamespaceConstants.OfficialShopNs,
            NamespaceConstants.BSellerNs, 
            NamespaceConstants.WuDiQuanNs,
            NamespaceConstants.TradeCenterNs);
  }
}

這個類裏標明有兩個業務方可能可以使用天貓積分: GuestNS,DefaultFictitiousNs,OfficialShopNs,BSellerNs,WuDiQuanNs, TradeCenterNs。

每個業務方都必須提供一個CanUseMallPoint這個接口的實現類,在這個類中實現具體的業務邏輯,比如對於無敵卡WuDiQuanNs的SPI實現:

@BizSpi
public class WuDiQuanCanUseMallPointSpi implements CanUseMallPoint {
  @Override
  public Boolean canUseMallPoint(OrderLineSDO orderLineSDO) {
    return true;
  }
}

這個接口實現根據業務需要,共有六個實現。

在這個簡單的功能點實現過程中,可以看到實現這個功能點對不同業務方的支持,我們更改了Orderline這個核心類,增加了一個新的接口CanUseMallPoint, 和新的Provider類CanUseMallPointProvider, 以及6個業務方的SPI實現類(WuDiQuanCanUseMallPointSpi等)。

以後當一個新業務要開發時,這樣的功能點,我們都要判斷是否需要針對該業務提供支持,如果需要,那要修改CanUseMallPointProvider類和增加一個SPI實現。

這隻是交易複雜邏輯中微小的一個功能點,可以看到對代碼的侵入很大,如果功能點是一個維度 X,業務方是另一個維度 Y,那可能的實現類就有XY乘積。我們可以看到代碼的膨脹以及以後代碼維護的代價。

如果按照Eclipse的Nature設計思想,TMallPoint(天貓積分)可以定義一個Nature,比如com.taobao.buy.tmallpointNature, 如果該訂單實例有這個Nature,就可以使用天貓積分。

那麼如何決定這個訂單是否應該有com.taobao.buy.tmallpointNature呢,這可以通過訂單識別來決定,不同的業務訂單,按照Nature的實際實現,對應的業務方可能會在訂單上加入對應的業務方Nature,比如無敵卡(WuDiQuan),會在訂單中增加com.taobao.buy.biz.wudiquanNature.

com.taobao.buy.tmallpointNature可以指定requires-nature:

  <requires-nature>
        id="com.taobao.buy.biz.wudiquanNature">
  </requires-nature>

那麼隻要項目具有無敵卡nature,就會自動有tmallpointNature,也就意味著可以有使用天貓積分的功能。

以上隻是一個簡單的例子,Eclipse有根複雜的引用,同樣交易平台也有更複雜的業務邏輯。
但是對比我們發現,Eclipse的方式,隻需要配置一些元數據和依賴關係,基本不需要增加新的代碼。功能點,訂單,業務的關係通過Nature這個標誌解耦後,可以方便的配置。

功能點->功能點nature->訂單<-業務nature<-業務

交易平台對多業務支持以及業務隔離的思考

目前TMF對多業務的支持以及業務隔離,會導致大量類的增加,比如一個新業務需求實現,需要對所有涉及到的功能點進行review並提供相應的SPI實現,同時新舊業務在同一個Provider裏,新業務開發者很難區分對舊業務功能是否有幹擾。

目前TMF對新業務的支持還是太重,對開發人員對TMF框架以及業務理解的要求很高,在平台化過程中,我們需要改進目前的缺點,借鑒Eclipse的方式,將業務,功能點通過Nature解耦,利用元數據信息動態管理,可以有效的減少業務代碼,從而也達到快速支持業務發展的目標。

IProject, IProjectFacet

另外在Eclipse WTP項目中(Webtooling Project)引入的Facet概念,可以說是對Nature的跟進一步增強。 具體可以了解: https://help.eclipse.org/luna/index.jsp?topic=%2Forg.eclipse.jst.j2ee.doc.user%2Ftopics%2Fcfacets.html

這是對交易平台化的初步探討,希望在我們做平台化過程中能吸收一些平台軟件的成功經驗。


最後更新:2017-04-01 13:37:10

  上一篇:go History of UNIX Project Build Tools
  下一篇:go 極致的 Hybrid 混合式開發(去啊App Hybrid 實戰)