NBearV3 Step by Step教程——IoC进阶篇

版本

1.3 [2006-11-12]

简介

本教程在《NBearV3 Step by Step教程——IoC》的基础上,演示如何基于NBearV3IoC模块开发一个分布式Web应用程序的过程。您将看到,基于NBearIoC组件,开发分布式系统就和开发单服务器系统一样容易。本教程同时将引导您注意分布式开发和非分布式开发,在实体定义中的注意事项。

1NBearV3提供的分布式支持,从用户视角来说,只要按照《NBearV3 Step by Step教程——IoC》的方式,以定义本地服务接口和实现相同的方法定义和实现服务接口,再进行一定的配置和部署,就能在不修改代码,甚至不需重新编译的情况下,使应用程序轻松具有分布式能力,并可以以Service为单位进行多服务器分布部署,且能够由ServiceMQ Server控制,自动实现负载均衡。在NBear封装的逻辑内部,是以ServiceMQ Server为消息中心,基于.Net Remoting进行消息传递,并使用Castle作为IoC容器实现的。

2:在阅读本文之前,建议读者先阅读《NBearV3 Step by Step教程——IoC》以掌握NBearV3中有关ORMIoC的基本知识。

目标

通过本教程,读者应能够全面掌握使用NBearV3IoC模块开发单服务器/分布式应用程序的全过程。

代码

本教程演示创建的所有工程和代码,包含于可以从sf.net下载的NBearV3最新源码zip包中的tutorials\IoC_Adv_Tutorial目录中。因此,在使用本教程的过程中如有任何疑问,可以直接参考这些代码。

时间

<30分钟。

正文

Step 1 下载NBearV3最新版本及准备

1.1访问http://sf.net/projects/nbear,下载NBearV3的最新版本到本地目录。

1.2 将下载的zip文件解压至C:\,您将看到,加压后的NBearV3目录中包括:distdoccasessrctutorials等目录。其中,在本教程中将会使用的是dist目录中的所有release编译版本的dllexetutorials目录中之前的IoC基础教程。

1.3 tutorials目录中的整个IoC_Tutorial目录复制到任意其它位置,并命名为IoC_Adv_Tutorial,我们将以IoC_Tutorial为基础,演示NBearV3中基于IoC的分布式开发的知识。

Step 2 扩展设计实体及元数据

2.1 IoC_Adv_Tutorial中的IoC_Tutorial.sln重命名为IoC_Adv_Tutorial.sln,并在VS2005开发环境中打开。

2.2在本教程中,对于从IoC_Tutorial继承过来的这些工程,我们会做很小的一些修改,您将注意到,我们做这些修改的原因,并不意味着,一个非分布式系统必须做经过修改才能以分布方式部署。而是,我们将引导您注意,在基于NBear的分布式系统中,实体定义和Service接口设计的重要注意事项。

2.3 首先,需要注意一个在分布系统中的实体设计规范:两个实体或者多个实体间,要避免双向/循环可读写、可序列化的引用

具体举例来说,如果您打开EntityDesigns中的EntityDesigns.cs文件,您将注意到,CategoryProduct,互相包含了可读写的引用。这会有什么问题呢?在非分布式系统中,只要两个引用不同时是LazyLoad=false,这就完全没问题,您在IoC Tutorial中已经看到了,程序运行得很正常。但是,在分布式情况下,因为,Service的中的方法的参数和返回值,会被序列化后,以消息的形式进行传递。所以,以这里的CategoryProduct为例,假如我有一个Product的实例,现在我把它序列化,此时会发生什么呢?他的属性Category也会被序列化,序列化这个Category属性时,又会发生什么呢?他的Products属性也要被序列化!!问题来了,我们最初的Product实例,肯定也包含在他的Category属性的Products中,所有又会被序列化。。。这样就死循环了。

怎么办呢?办法很简单,至少将一个引用设为只读(设为只有get没有set)或不可序列化(为属性标注SerializationIgnoreAttribute)。在这个CategoryProduct的关系中,比较合理的是将Category.Products属性设为只读,代码如下:

    [MappingName("Categories")]
    
public interface Category : Entity
    
{
        [PrimaryKey]
        
int CategoryID get; }
        [SqlType(
"nvarchar(15)")]
        
string CategoryName getset; }
        [SqlType(
"ntext")]
        
string Description getset; }
        
byte[] Picture getset; }

        [FkQuery(
"Category", OrderBy = "{ProductName}", Contained = true, LazyLoad = true)]
        [SerializationIgnore]
        Product[] Products
        
{
            
get;
            
set;
        }

    }

此时,序列化Category时,就不会序列化他的Products,从而就能避免序列化的死循环。也就能正常用于分布式系统了。

Step 3 从实体设计代码生成实体代码、实体配置文件

3.1 至此,所有的实体的设计就修改就完毕了。编译EntityDesigns工程。

3.2 运行dist目录中的NBear.Tools.EntityDesignToEntity.exe工具,载入EntityDesigns工程编译生成的EntityDesigns.dll

3.3 点击Generate Entities按钮,将生成的代码保存到Entities工程中的一个名叫Entities.cs的代码文件。

3.4 点击Generate Configuration按钮,将生成的代码保存到website工程下的名为EntityConfig.xml的文件中。

Step 4 使用ServiceMQServer.exeServiceHost.exe,部署程序为分布式系统

4.1 在将测试程序部署为分布式系统之前,我们先验证一下程序运行正常。将website设为启动工程,并设置Default.aspx为启动页。运行website,看看,Default.aspx是否正常显示了和IoC_Tutorial中完全相同的运行结果。

4.2 为了更方便测试,我们在IoC_Adv_Tutorial目录中建一个Bin目录,Bin目录中建立ServiceMQServer目录和ServiceHost目录。新建如下的脚本文UpdateAssemblies.bat,用来更新所有需要的dllexe到两个目录下:

@echo off
copy ..\website\EntityConfig.xml .\ServiceHost\ /Y
copy ..\Entities\bin\Debug\*.* .\ServiceHost\ /Y
copy ..\ServiceImpls\bin\Debug\*.* .\ServiceHost\ /Y
copy ..\ServiceInterfaces\bin\Debug\*.* .\ServiceHost\ /Y
copy ..\..\..\dist\NBear.IoC.Servers.ServiceMQServer.exe .\ServiceMQServer\ /Y
copy ..\..\..\dist\NBear.Common.dll .\ServiceMQServer\ /Y
copy ..\..\..\dist\NBear.IoC.dll .\ServiceMQServer\ /Y
copy ..\..\..\dist\NBear.Net.dll .\ServiceMQServer\ /Y
copy ..\..\..\dist\NBear.IoC.Hosts.ServiceHost.exe .\ServiceHost\ /Y

4.3 执行4.2所见的脚本,复制相关程序集到这两个目录。

4.4 ServiceMQServer目录中,我们看到,除了NBear.*.dll之外,只有一个文件NBear.IoC.Servers.ServiceMQServer.exe。这个文件是NBear提供的,从dist目录复制过来的。我们需要为它创建如下的NBear.IoC.Servers.ServiceMQServer.exe.config文件:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 
<configSections>
    
<section name="serviceFactory" type="NBear.IoC.Service.Configuration.ServiceFactoryConfigurationSection, NBear.IoC" />
 
</configSections>
 
<serviceFactory type="Remoting" name="testServiceFactory" protocol="HTTP" server="127.0.0.1" port="8888" debug="true" maxTry="30" />
</configuration>

以上的配置,指定了允许连接到该ServerServiceFactory的配置信息。其中参数含义分别为:

· type - ServiceFactory的类型是Remoting,默认情况下,ServiceFactory的类型总是Local的,所以不能连接远程ServiceMQServer

· name – 用于连接ServiceMQServer的唯一名称,该名称不能包含空格

· protocol - ServiceFactory连接ServiceMQServer的协议,可选值为HTTPTCP

· serverport – ServiceMQServer监听的服务器地址和端口。

· debug - 是否在ServiceMQServer中显示调试日置信息。

· maxTry - 对于同一个消息的等待读取的最大次数。

4.5 我们再切换到ServiceHost目录。该目录下包含了用于部署Service的程序集。我们可以看到,有ServiceInterfaces.dllServiceImpls.dllEntities.dll,相关的NBearCastke程序集,和NBear.IoC.Hosts.ServiceHost.exe。最后这个程序也是有NBear提供,从dist复制过来的。我们需要为它创建如下的NBear.IoC.Hosts.ServiceHost.exe.config文件:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  
<configSections>
    
<section name="castle"
        type
="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, Castle.Windsor" />
    
<section name="serviceFactory" type="NBear.IoC.Service.Configuration.ServiceFactoryConfigurationSection, NBear.IoC" />
    
<section name="entityConfig" type="NBear.Common.EntityConfigurationSection, NBear.Common"/>
  
</configSections>
  
<entityConfig>
    
<includes>
      
<add key="Sample Entity Config" value="~/EntityConfig.xml"/>
    
</includes>
  
</entityConfig>
  
<castle>
    
<components>
      
<!--You can use standard castle component decleration schema to define service interface impls here-->
      
<component id="category service" service="ServiceInterfaces.ICategoryService, ServiceInterfaces" type="ServiceImpls.CategoryService, ServiceImpls"/>
      
<component id="product service" service="ServiceInterfaces.IProductService, ServiceInterfaces" type="ServiceImpls.ProductService, ServiceImpls"/>
    
</components>
  
</castle>
  
<serviceFactory type="Remoting" name="testServiceFactory" protocol="HTTP" server="127.0.0.1" port="8888" debug="true" maxTry="30" />
  
<connectionStrings>
    
<add name="Northwind" connectionString="Server=(local);Database=Northwind;Uid=sa;Pwd=sa" providerName="NBear.Data.SqlServer.SqlDbProvider"/>
  
</connectionStrings>
</configuration>

我们可以注意到,配置文件中除了包含对ServiceFactory的配置,参数的含义和4.4中的config含义完全一样。另外,这里也包含了我们从IoC_Tutorial中复制过来的website中的Web.config中类似的entityConfigConnectionStringcastke配置节。之所以要配置这些信息,是因为,我们的ServiceHost将作为Service的宿主,接受对他支持的service的访问请求,要读取实体信息,也需要访问数据库。

4.6 接着,为了让website能够访问远程Service,我们需要为websiteWeb.config添加serviceFactory配置节,同时为,为了演示同时存在本地Service和远程Service的情形,我们保留castle配置节中的category service。修改完的Web.config内容如下:

<?xml version="1.0"?>
<configuration>
    
<configSections>
    
<section name="serviceFactory" type="NBear.IoC.Service.Configuration.ServiceFactoryConfigurationSection, NBear.IoC" />
    
<section name="entityConfig" type="NBear.Common.EntityConfigurationSection, NBear.Common"/>
    
<section name="castle" type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, Castle.Windsor"/>
  
</configSections>
    
<entityConfig>
        
<includes>
            
<add key="Sample Entity Config" value="~/EntityConfig.xml"/>
        
</includes>
    
</entityConfig>
  
<castle>
    
<components>
      
<!--You can use standard castle component decleration schema to define service interface impls here-->
      
<component id="category service" service="ServiceInterfaces.ICategoryService, ServiceInterfaces" type="ServiceImpls.CategoryService, ServiceImpls"/>
    
</components>
  
</castle>
  
<serviceFactory type="Remoting" name="testServiceFactory" protocol="HTTP" server="127.0.0.1" port="8888" debug="true" maxTry="30" />
  
<appSettings/>
    
<connectionStrings>
        
<add name="Northwind" connectionString="Server=(local);Database=Northwind;Uid=sa;Pwd=sa" providerName="NBear.Data.SqlServer.SqlDbProvider"/>
    
</connectionStrings>
    
<system.web>
        
<compilation debug="true">
            
<assemblies>
                
<add assembly="System.Transactions, Version=2.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
                
<add assembly="System.Data.OracleClient, Version=2.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
                
<add assembly="System.Runtime.Remoting, Version=2.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/></assemblies></compilation>
        
<authentication mode="Windows"/>
    
</system.web>
</configuration>

4.6 如果您想在多个服务器上测试本程序,您可以分别将ServiceMQServerServiceHost目录中的内容复制到不同的服务器。但是,需要注意修改所有的config中的server地址修改为ServiceMQServer所在的服务器地址。

当然,如果只是想先看看运行效果,你也可以直接在本机运行。

Step 5 运行分布式程序

5.1 现在我们就可以运行整个程序了。我们首先必须先运行ServiceMQServer.exe

5.2 接着,我们运行两个ServiceHost.exe实例(如果您愿意,也可以运行更多)。您将能看到,在ServiceMQServer.exe的窗口中,会显示,分别由两个ICategoryServiceIProductService的订阅者。他们自然是我们的ServiceHostServiceMQServer订阅的。

5.3 运行website,并访问Default.aspx,你将能看到website的运行结果应该和没有部署为分布式程序之前的结果实完全一样的。您可以刷新几次页面,并注意ServiceMQServerServiceHost的窗口。

您将能看到,对IProductService的请求,会被自动发送给两个ServiceHost中的一个来处理并返回,但是,你看不到ICategoryService被处理的日志。为什么呢?因为,我们在websiteWeb.config中的castle块中保留了本地的category service组件定义。在Default.aspx请求某个Service时,如果,ServiceFactory发现有本地实现,则会直接返回本地Service实现的实例,如果找不到本地实现,则会向ServiceMQServer发送Service调用请求,ServiceMQServer,则将把对Service的调用请求负载均衡地,转发给注册到它的ServiceHost。所以,在多刷新几次页面的时候,您将注意到,有时,请求是被一个ServiceHost处理的,有时,请求被另一个处理。

如果你将Web.config中的category service那个component注释掉,再次刷新Default.aspx页面,则您将能看到,对category service调用,也会被发送给ServiceHost处理。

但是,注意,此时,Category.Products总是返回null。为什么呢?因为,我们在2.3中将Category.Products属性设为只读了。只读属性是不会被序列化的,所以Products不会被传递到远程。

正文结束。

附录

1 关于分布式系统中LazyLoad=true的属性

有朋友问,实体被分布式的传递到远程后,LazyLoad=true的属性被访问时,会是什么行为呢?

实际上,请注意一个事实——那就是实体或实体数组总是在序列化后,才被发送到远程的,所以,至少,serializer会访问一次被序列化的实体的可读写属性,也因此,被接收到的远程实体的属性,即使是LazyLoad=true的属性,它们的内容其实已经被Load过了,如果在访问这样的属性,只是简单的返回已载入的数据。

2 关于ServiceFactory返回远程Service访问代理的内部原理

相信更多朋友对ServiceFactory如何返回远程Service访问代理,以及访问代理的内部原理非常感兴趣。

限于篇幅,我这里只是简单介绍一下一个Service调用处理过程——从调用端发出调用请求,到请求端收到处理结果的过程。

Default发出一个Service调用请求,如IProductService.GetAllProducts()之前,它首先要从ServiceFactory.GetService<>()得到一个IProductService的实现类。ServiceFactory首先判断是否有一个本地Service实现注册在自己的Web.config中,如果有,对于本地Service实现组件而言,ServiceFactory简单的返回一个新建的实现类的实例。

ServiceFactory找不到本地Service实现时,他将在内存中,使用System.Reflection.Emit技术,动态创建一个实现了IProductService的代理类(第一次创建后会缓存起来),这个代理类封装了对ServiceMQServer的访问功能。调用一个代理类的方法的过程为:代理类奖输入参数序列化,并封装为一个RequestMessage,发送给ServiceMQServer,并定时查询ServiceMQServer是否已经处理完毕;ServiceMQServer接到RequestMessage,则负载均衡地将RequestMessage转发给注册到它的某一个能够处理IProductServiceServiceHostServiceHost接到ServiceMQServer的通知时,执行Service逻辑,并将结果返还给ServiceMQServer;此时,代理类发现Service请求已经处理完毕;它就将结果取回来,返回给调用者。

3 关于自定义序列化逻辑

默认情况下,在分布式Service的方法的参数或返回值都会被序列化为XML。有一些情形下,我们需要自定义序列化方式,或者,还有一些情况下,某些参数会返回值类型默认不能被序列化,比如接口类型,那么,这些情况下,我们都需要为这些类型定义自定义序列化/反序列化逻辑。

我们可以使用NBear.Common.SerializationManager类来自定义特定类型的序列化方式。一般,我们可以在应用程序启动的时候,如Web应用程序的Application_Start中,调用SerializationManager.RegisterSerializeHandler()/UnregisterSerializeHandler()方法注册和注销对特定类型的自定义序列化和反序列化方法。

关于自定义ServiceMQServerServiceHost

NBear默认提供的ServiceMQServerServiceHost都是非常简单的控制台程序实现,它们的有效源码分别都不到10行。它们对于日志也只是简单的显示出来。在现实的开发中,这往往会不能满足我们的需求,此时,我们可以参照这两个程序的源码,实现您自己的ServiceMQServerServiceHost。比如,我们可以将它们写成Windows Service,或者Windows Form程序。

//本文结束

posted @ 2006-11-07 16:39  Teddy's Knowledge Base  Views(4667)  Comments(11Edit  收藏  举报