平台化三部曲之一微核心可扩展架构 - 从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采用“平台+插件”的体系结构,平台仅仅作为一个容器,所有的业务功能都封装在插件中,通过插件组件构建开发环境。

平台化三部曲之一微核心可扩展架构 - 从Eclipse平台看交易平台化

Platform Runtime平台运行库是内核,Eclipse所有的功能就是通过这个runtime和插件一起完成。 在这个runtime里,定义Eclipse的核心功能。在下面的章节,会介绍Eclipse Runtime扩展实现的机制。通过学习这些机制,我们可以学到以下经验:

  1. Eclipse的核心类是怎样做到保持稳定,同时又怎样支持以后得各种功能扩展。
  2. Eclipse如何对多业务的支持和新业务的快速接入。

首先我们来认识一个非常重要的接口:

IAdapable

在Eclipse SDK版本下,用F4查看IAdaptable的类继承关系,你会发现IAdaplabe接口使用的是多么广泛,基本上Eclipse的核心类都继承这个接口。
平台化三部曲之一微核心可扩展架构 - 从Eclipse平台看交易平台化
如果你想了解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的跟进一步增强。 具体可以了解: http://help.eclipse.org/luna/index.jsp?topic=%2Forg.eclipse.jst.j2ee.doc.user%2Ftopics%2Fcfacets.html

这是对交易平台化的初步探讨,希望在我们做平台化过程中能吸收一些平台软件的成功经验。


上一篇:换个视角看Maven - 一个领域平台的优美设计


下一篇:java基本类型