技术开发 频道

基于Jazz技术构建企业级Web2.0应用(2)

 《基于Jazz技术构建企业级Web2.0应用》系列第一部分:第1部分:数据模型设计与持久化

【IT168 专稿】

    在本系列文章的第一部分里详细介绍了怎样基于Jazz提供的O-R mapping机制来实现数据层的持久化。本文将详细描述怎样利用Jazz平台提供的基础服务来为我们的企业级应用提供业务服务层支持的全过程。通过阅读本文,你将了解如何来构建基于Jazz的RPC服务、REST服务以及DTO设计。文章的最后还阐述了怎样基于Jazz提供的测试框架来对服务层进行单元测试。

Jazz服务层介绍

    构建在Jazz平台上的服务分为四类:

    · RPC服务:通常负责底层业务逻辑的实现,如对数据进行增、删、改、查(C/R/U/D)操作,类似于J2EE开发中的数据访问对象(Data Access Object, DAO)

    · REST服务:REST服务是一种无状态的轻量级的Web服务,主要负责向客户端(如Web客户端)提供REST风格的服务,一般构建在RPC服务层之上并通过调用RPC服务实现其业务功能,并把处理结果以SOAP的消息形式返回给客户端。

    · HTTP:负责直接处理和响应http请求(如HTTP POST GET DELETE等)类似于Servlet

    · CONTENT:主要负责二进制数据类型content(Jazz定义的特殊数据类型)的存取,如用于实现附件上传等功能。Jazz会将这些二进制数据储存于文件系统或者数据库的BLOB字段中。

PetStore服务层介绍

    在一个典型的Jazz Web应用中,通常需要实现一组RPC服务实现对数据的操作,并在此基础上通过REST服务向Web客户端提供服务接口。本文将以PetStore为例介绍如何实现Jazz平台上的服务实现和测试。内容包括:

    · 实现一组RPC服务以完成PetStore中各种数据类型的CRUD功能。

    · 实现一个REST服务IPetStoreRestService,封装RPC服务的功能供给Web Client调用,实现宠物出售、查询、购买等业务逻辑。

    · 通过一个JUnit插件项目实现对RPC服务和REST服务层的单元测试。

PetStore RPC服务的定义和实现

    在Jazz服务开发中,通常需要为每种数据类型实现一个RPC服务提供CRUD功能。下面我们以PetStore中的IProductService为例介绍RPC服务定义和实现。

    PetStore RPC服务涉及两个bundle,其中RPC服务的定义在bundle com.ibm.petstore.common中,RPC服务的具体实现在bundle com.ibm.petstore.service中。具体步骤如下

    1.在com.ibm.petstore.common中定义RPC服务的接口,如清单1所示

    清单1. IProductService服务接口定义

    public interface IProductService {
    //创建或修改宠物信息
    public IProduct createProduct(int id, String name, String description,
        ICategoryHandle category, ISellerHandle seller, int price,
        String tags) throws TeamRepositoryException;
    //保存宠物信息   
    public IProduct saveProduct(IProduct workingCopy)
        throws TeamRepositoryException;
    //查找所有的宠物
    public IProduct[] findAllProducts() throws TeamRepositoryException;
    //查找id对应的宠物
    public IProduct findProductById(int id) throws TeamRepositoryException;
    }

    注意:服务中每个方法都需要抛出TeamRepositoryException。

    2.在com.ibm.petstore.common的plugin.xml中进行RPC服务的注册, 如清单2所示

    清单2. IProductService服务接口注册

    <service
        kind="RPC"
        name="IProductService"
        uri="com.ibm.petstore.common.service.IProductService"
        version="1">
    </service>

    注意:这里“kind”属性为“RPC”,表示注册的是一个RPC服务,“uri”属性为第(1)步中定义的服务接口,Jazz在服务器启动时会自动地对这个RPC服务进行实例化。

    3.在com.ibm.petstore.service中实现各个RPC服务,如清单3所示

    清单3. IProductService服务实现

    public class ProductService extends AbstractService implements
        IProductService {
        ……
    }

    注意:RPC服务的实现类需要继承com.ibm.team.repository.service.AbstractService,并实现第(1)步中定义的服务接口。其中AbstractService是Jazz中所有服务的抽象基类,提供了服务的上下文、服务绑定以及一些公用的基础设施。

    4.在com.ibm.petstore.service的plugin.xml中进行RPC服务和实现类的绑定,如清单4所示

    清单4. IProductService服务实现注册

    <serviceProvider
    componentId="com.ibm.petstore"
    implementationClass="com.ibm.petstore.service.ProductService">
        <provides>
            <providedService
            interface="com.ibm.petstore.common.service.IProductService">
            </providedService>
        </provides>
        <prerequisites>
            <requiredService
                interface="com.ibm.team.repository.common.service.IQueryService">
            </requiredService>
            <requiredService
                interface="com.ibm.team.repository.service.IRepositoryItemService">
            </requiredService>
         </prerequisites>
    </serviceProvider>

    在服务实现的注册中我们将服务com.ibm.petstore.common.service.IProductService与其实现类com.ibm.petstore.service.ProductService绑定,并通过requiredService字段声明了其依赖的两个服务com.ibm.team.repository.common.service.IQueryService和com.ibm.team.repository.service.IRepositoryItemService,这样Jazz在加载一个IProductService服务的实例前会先为其加载这两个服务的实例。

    在调用依赖的服务时,可以使用Jazz提供的依赖注入机制,通过getService方法(由RPC服务实现类的基类com.ibm.team.repository.service.AbstractService提供)获得任何一个服务实例,然后就可以使用该服务了。清单5展示了在ProductService类中获得IRepositoryItemService和IQueryService实例来实现查找所有宠物的代码片段:

    清单5. getService方法示例

    public IProduct[] findAllProducts() throws TeamRepositoryException {
    IQueryService queryService = getService(IQueryService.class);

    ProductQueryModel queryModel = BaseProductQueryModel.ProductQueryModel.ROOT;
    IItemQuery itemQuery = IItemQuery.FACTORY.newInstance(queryModel);

    IItemQueryPage itemQueryPage = queryService.queryItems(itemQuery,
         IQueryService.EMPTY_PARAMETERS,
         IQueryService.ITEM_QUERY_MAX_PAGE_SIZE);

    IRepositoryItemService repositoryItemService =
         getService(IRepositoryItemService.class);
    IItem[] items = repositoryItemService.fetchItems(itemQueryPage
            .handlesAsArray(), null);
    ArrayList<IProduct> products = new ArrayList<IProduct>();
    for (IItem item : items) {
        if (item instanceof IProduct) {
            products.add((IProduct) item);
        }
    }

    return (IProduct[]) products.toArray(new IProduct[products.size()]);
}

RPC服务中数据CRUD的实现

    在RPC服务中可以通过Repository构件提供的两个服务com.ibm.team.repository.service.IRepositoryItemService和com.ibm.team.repository.common.service.IQueryService来实现数据项的CRUD功能。IRepositoryItemService提供了Jazz存储库中数据项的持久化功能,IQueryService主要用于对数据项的查询。

    下面将结合PetStore中ProductService的实现来介绍如何利用这两个服务实现数据的CRUD操作。

    1.创建和保存宠物,如清单6所示

    清单6. 创建并保存宠物

    public IProduct createProduct(int id, String name, String description,
        ICategoryHandle category, ISellerHandle seller, int price,
        String tags) throws TeamRepositoryException {
    IProduct product = (IProduct) IProduct.ITEM_TYPE.createItem();
    product.setId(id);
    product.setName(name);
    product.setDescription(description);
    product.setCategory(category);
    product.setSeller(seller);
    product.setPrice(price);       
    product.setTags(tags);

    return product;
}

    public IProduct saveProduct(IProduct workingCopy)
    throws TeamRepositoryException {
        ……
        IProduct product = getService(IRepositoryItemService.class).saveItem(
        workingCopy);
        return  product;
    }

    在实现中,我们分别通过createProduct和saveProduct方法实现宠物信息的创建和保存。注意:我们在saveProduct中通过调用IRepositoryItemService的saveItem方法完成一个产品数据项的保存。Repository组件会以O-R映射的形式,在底层生成对应的insert into SQL语句,调用JDBC完成数据持久化。

    2.根据ID获取宠物信息,如清单7所示

    清单7. 查询宠物信息

public IProduct findProductById(int id) throws TeamRepositoryException {
    Object[] parmValues = new Object[] { id };
    IQueryService queryService = getService(IQueryService.class);

    ProductQueryModel queryModel = BaseProductQueryModel.ProductQueryModel.ROOT;
    IItemQuery itemQuery = IItemQuery.FACTORY.newInstance(queryModel);

    IPredicate predicate = queryModel.id()._eq(itemQuery.newIntegerArg());
    itemQuery.filter(predicate);

    IItemQueryPage itemQueryPage = queryService.queryItems(itemQuery,
            parmValues, 10);

    IRepositoryItemService repositoryItemService =
         getService(IRepositoryItemService.class);
    IItem[] items = repositoryItemService.fetchItems(itemQueryPage
            .handlesAsArray(), null);

    if (items.length > 1) {
        throw new TeamRepositoryException(
                "More than one product found with id " + id);
    } else if (items.length == 1) {
        return (IProduct) items[0];
    }
    return null;
}

    从findProductById方法的实现中,我们可以知道对数据的查询包含如下步骤:

    · 首先根据数据模型ProductQueryModel构造了一个查询itemQuery,ProductQueryModel是在第一部分构建数据持久化模型时,由Jazz自动生成。它代表了实体的查询模型,例如描述了哪些字段是可以搜索的,以及这些字段的类型。

    · 接着构造了一个查询谓词predicate设定查询条件。例如在findProductById中,查询条件为数据项的id值必须等于传入的参数。

    · 然后使用IQueryService的queryItems方法进行查询,获得符合查询条件的数据项的句柄。在Jazz的底层会根据我们的数据存储模型和对应的查询条件,生成相应的SQL语句,并调用JDBC完成搜索过程。

    · 最后调用IRepositoryItemService的fetchItems方法获取句柄对应的数据实体对象。

    3.更新宠物数据

    在Jazz中如果需要对数据实体进行更新,则需要在数据的工作拷贝上进行,一个数据项的更新包括几个步骤(如清单8所示):

    · 获取数据项的工作拷贝,

    · 并调用setter方法更新其内容

    · 持久化数据项

    清单8. 更新宠物数据

    IProduct productWorkingCopy=productItemHandle. getWorkingCopy();
    productWorkingCopy.setName("new name");

    ……
    productService.saveProduct(productWorkingCopy);

    4.删除宠物

    上面介绍了IRepositoryItemService提供saveItem方法持久化一个数据项。类似的,它提供deleteItem方法删除一个数据项,如清单9所示。

    清单9. 删除宠物

    repositoryItemService.deleteItem(productItemHandle);

PetStore REST服务的定义和实现

    在Jazz Web应用中,开发者可以通过REST服务向Web客户端暴露服务器端提供的各种服务。在PetStore应用我们实现了REST服务IPetStoreRestService向Web UI提供其所需的功能。下面我们介绍IPetStoreRestService的定义和实现。

    1.定义数据传输对象DTO

    数据传输对象是指在客户端和服务器端进行数据交换用的数据对象,Web客户端调用REST服务的getXXX方法获取数据时,REST服务将调用RPC服务获取数据并将结果封装成一个DTO返回,Jazz平台会将其转换成一个JSON(JavaScript Object Notation)数组传递给Web客户端。

    我们新建一个Ecore数据模型petstoreDTO.ecore来定义每个数据传输对象DTO,如Product对应的DTO模型如图 1所示:


    图1. Product DTO

    注意:与需要持久化的数据不同,DTO数据仅仅在传输中使用。所以在实现时,DTO模型的定义只需包含传递给客户端的数据内容,无需添加持久化相关的注解(annotation)。通过运行Jazz codegen,可以根据DTO模型生成对应的Java 类。

    2.在com.ibm.petstore.common中定义REST服务接口,如清单10所示。

    清单10. REST服务接口定义

    public interface IPetStoreRestService extends ITeamModelledRestService{
        public ProductDTO[] getAllProductDTOs() throws TeamRepositoryException;
   
        public ProductDTO postProduct(ParametersPublishProduct param) throws TeamRepositoryException;

        public static final class ParametersPublishProduct implements IParameterWrapper{
            public String categoryId;
            public String name;
            public String description;
            public String price;
            public String tags;//tag1,tag2,tag3,...,tagN
            public String sellerName;
            public String sellerAddress;
            public String sellerEmail;
        }
        ……
    }

    注意:REST服务接口定义需要继承ITeamModelledRestService, Jazz中所有的REST服务必须集成此接口。

    各个方法名称遵循REST风格getXXX、postXXX……,分别用于处理客户端对应的get、post等请求响应。

    每个postXXX方法的参数都需要运用IParameterWrapper来进行参数封装

    每个方法都需要抛出TeamRepositoryException。

    3.在com.ibm.petstore.common的plugin.xml中进行REST服务的注册,如清单11所示。

    清单11. REST服务接口注册

    <service
        kind="MODELLED_REST"
        name="PetStore Item Rest Service"
        uri="com.ibm.petstore.common.service.rest.IPetStoreRestService"
        version="1">
    </service>

    注意:“kind”属性为“MODELLED_REST”,这样Jazz平台就能将其注册为REST服务。

    4.在com.ibm.petstore.service中实现REST服务,如清单12所示。

    清单12. REST服务实现

    public class PetStoreRestService extends AbstractService implements IPetStoreRestService {
    ……
    }

    这里我们通过PetStoreRestService中宠物发布postProduct和获取所有宠物getAllProductDTOs的实现介绍REST服务如何实现与Web客户端的数据交互,如清单13所示。

    清单13. 响应浏览器post事件

public ProductDTO postProduct(ParametersPublishProduct param) throws
    TeamRepositoryException {
    ……
    IProduct product=getService(IProductService.class).createProduct(0,
         param.name, param.description, category, seller, price, tags);
    product=getService(IProductService.class).saveProduct(product);   
     return productToDTO(product);
}

public ProductDTO[] getAllProductDTOs() throws TeamRepositoryException {
    IProduct[] products=getService(IProductService.class).findAllProducts();
    ProductDTO[] dtos=new ProductDTO[products.length];
    for(int i=0;i<products.length;i++)
        dtos[i]=productToDTO(products[i]);
    return dtos;
}

    可以看到,postProduct方法接收客户端传来的Http Request 参数并调用RPC服务IProductService创建和保存宠物,最终将创建的宠物转换为DTO返回给客户端。getAllProductDTOs方法通过调用IProductService获取所有的产品并将它们转换为DTO数组返回给客户端。productToDTO方法实现如清单 14所示:

    清单14. 向客户端返回DTO数据

private ProductDTO productToDTO(IProduct product)
            throws TeamRepositoryException {
        ProductDTO dto = RestFactory.eINSTANCE.createProductDTO();
        dto.setId(product.getId());
        dto.setName(product.getName());
        dto.setDescription(product.getDescription());
        dto.setPrice(product.getPrice());
        dto.setTags(product.getTags());
        CategoryDTO cdto = categoryToDTO((ICategory) getService(
                IRepositoryItemService.class).fetchItem(product.getCategory(),
                null));
        dto.setCategory(cdto);
        SellerDTO sdto = sellerToDTO((ISeller) getService(
                IRepositoryItemService.class).fetchItem(product.getSeller(),
                null));
        dto.setSeller(sdto);
        return dto;
}

PetStore服务的测试

    下面我们在PetStore中增加一个插件项目com.ibm.petstore.service.tests用于RPC服务和REST服务的单元测试。

    1.创建service test bundle

    File->New Plug-in Project ,给project取名为com.ibm.petstore.service.tests。

    设置目标环境为Eclipse,不创建Activator和UI,不选择Rich Client Application。

    2.插件依赖设置

    修改MANIFEST.MF,在Dependencies 选项卡中添加我们将需要的插件依赖关系,如图2所示

    · org.junit
    · com.ibm.petstore.common
    · com.ibm.petstore.service
    · com.ibm.team.repository.common
    · com.ibm.team.repository.service
    · com.ibm.team.repository.service.tests


    图2. 测试插件依赖设置

    3.编写测试代码

    编写测试代码需要针对每一个服务编写Test Case,同时提供一个Test suite用于测试执行。

    · 建立Test Case

    针对每一个RPC服务和REST服务建立相应的测试用例。值得注意的是,测试用例类的不是继承自junit.framework.TestCase,而是继承自com.ibm.team.repository.service.tests.AbstractRemoteServiceTestCase。

    AbstractRemoteServiceTestCase是Jazz服务器端测试用例的基础类,它提供了几个有用的方法,其中最重要的是getService。通过getService方法可以获取需要测试的服务接口,并通过调用服务接口的方法进行测试。

    下面以ICategoryService 服务接口为例探讨测试用例的编写,如清单15所示。

    清单15. ICategoryService服务接口的测试用例

package com.ibm.petstore.service.tests;

import com.ibm.petstore.common.ICategory;
import com.ibm.petstore.common.service.ICategoryService;
import com.ibm.team.repository.common.TeamRepositoryException;
import com.ibm.team.repository.service.tests.AbstractRemoteServiceTestCase;

public class CategoryServiceTest extends AbstractRemoteServiceTestCase {

    private ICategory _tempCategory;
    public CategoryServiceTest() {
    }

    public CategoryServiceTest(String name) {
        super(name);
   }
   
    @Override
    protected void setUp() throws Exception {
        super.setUp();
        ICategoryService service = getService();
        ICategory category = service.createCategory(12345, "small", "the small animal");
        _tempCategory = service.saveCategory(category);
    }
   
    public ICategoryService getService()
    {
        return getService(ICategoryService.class);
    }
   
    public void testFindCategoryById() throws TeamRepositoryException
    {
        ICategoryService service = getService();
        int id = _tempCategory.getId();
        ICategory category = service.findCategoryById(id);
        this.assertEquals(category.getItemId(), _tempCategory.getItemId());  
    }
     
    public void testFindAllCategories() throws Exception {
        ICategoryService service = getService();
        int nCategoryNumOld = service.findAllCategories().length;
       
        // create a new category
        ICategory category = service.createCategory(12345, "small", "the small animal");
        category = service.saveCategory(category);
        int nCategoryNumNew = service.findAllCategories().length;
       
        // the number of categories has been increased
        assertEquals(nCategoryNumOld + 1, nCategoryNumNew);
    }
    …

}

    可以看出测试过程中,首先通过getService方法获取ICategoryService服务接口,然后对ICategoryService中的方法逐一进行测试。

    值得注意的是可以在setup中做一些前期的工作,例如创建临时的Category,这样可以用于测试查询功能。

    · 建立TestSuite,如清单16所示。

    清单16. TestSuite

public class AllTests {

    public static Test suite() {
        TestSuite suite = new TestSuite(
                "Test for com.ibm.petstore.service.test");
        //$JUnit-BEGIN$
        suite.addTestSuite(CategoryServiceTest.class);
        suite.addTestSuite(ProductServiceTest.class);
        suite.addTestSuite(SellerServiceTest.class);
        suite.addTestSuite(PetStoreRestServiceTest.class);
        //$JUnit-END$
        return suite;
    }
}

    4.测试执行

    测试执行通过创建一个JUnit Plug-in Test来执行单元测试,具体步骤如下:

    1)创建Launcher

    Run->Open Run Dialog,创建一个JUnit Plugin-in Test  launcher。

    2)设置测试类

    选择项目为:com.ibm.petstore.service.tests,

    选择Test Class为:com.ibm.petstore.service.tests.AllTests

    3)设置启动模式

    使用[No Application] - Headless Mode作为要运行的应用程序

    4)设置VM参数,如清单17所示

    清单17. VM参数设置

    -Dcom.ibm.team.core.tests.repo=local -Dcom.ibm.team.repository.db.jdbc.location=${target_home}/../../server/petstore/repositoryDB

    第一个参数指定数据库使用本地模式,第二个参数标识 Jazz 存储库数据库的位置。上面显示的数据库路径应该指向缺省数据库。(这里的数据库路径与创建数据库的路径一致)。

    5)设置运行所需的Plug-ins

    选择all workspace and enabled target plug-ins通过以上设置就可以保证服务测试插件的执行了。

总结

    本文通过PetStore应用实例介绍了基于Jazz平台的服务定义、实现和测试。运用Jazz平台,可以快速地构建具有REST风格的服务,从而实现企业级的Web2.0应用。

参考资料

    · https://jazz.net/learn/LearnItem.jsp?href=content/docs/platform-overview/index.html

    · 关于Jazz REST服务请参考https://jazz.net/wiki/bin/view/Main/JazzRESTServicesMain

0
相关文章