我给MIS类型的软件分四个实现层次,三层架构。
BusinessLogic 业务实体 由LLBL Gen 生成业务实体,代码生成器生成
Interface 数据访问接口 根据实体产生的数据访问接口,由Code Smith生成
Manager 接口实现 根据实体产生的数据访问接口的实现代码,由Code Smith生成
UI 界面层 拖拉控件,绑定数据到界面中
以ORM作为数据访问基础技术,业务实体中包含数据之间的关系逻辑,而不再是用于填充数据的实体。
以上结构由LLBL Gen自动生成,它已经为我们生成了实体,实体验证类型,数据访问接口和相关的辅助类型。
公司注册中的公司实体,它的定义如下代码所示
[Serializable] public partial class CompanyEntity : CommonEntityBase // __LLBLGENPRO_USER_CODE_REGION_START AdditionalInterfaces // __LLBLGENPRO_USER_CODE_REGION_END
{ #region Class Member Declarations private EntityCollection<ModuleEntity> _modules; // __LLBLGENPRO_USER_CODE_REGION_START PrivateMembers // __LLBLGENPRO_USER_CODE_REGION_END #endregion #region Statics private static Dictionary<string, string> _customProperties; private static Dictionary<string, Dictionary<string, string>> _fieldsCustomProperties; /// <summary>All names of fields mapped onto a relation. Usable for in-memory filtering</summary> public static partial class MemberNames { /// <summary>Member name Modules</summary> public static readonly string Modules = "Modules"; } #endregion /// <summary> Static CTor for setting up custom property hashtables. Is executed before the first instance of this entity class or derived classes is constructed. </summary> static CompanyEntity() { SetupCustomPropertyHashtables(); } /// <summary> CTor</summary> public CompanyEntity():base("CompanyEntity") { InitClassEmpty(null, null); } ..... }
LLBL Gen设计器生成的实体代码有几个特点
[EditorBrowsable(EditorBrowsableState.Never)] protected CompanyEntity(SerializationInfo info, StreamingContext context) : base(info, context) { if(SerializationHelper.Optimization != SerializationOptimization.Fast) { _modules = (EntityCollection<ModuleEntity>)info.GetValue("_modules", typeof(EntityCollection<ModuleEntity>)); this.FixupDeserialization(FieldInfoProviderSingleton.GetInstance()); } // __LLBLGENPRO_USER_CODE_REGION_START DeserializationConstructor // __LLBLGENPRO_USER_CODE_REGION_END }
这个构造方法用在序列化对象时发生,比如.net Remoting远程返回对象时。
private static Dictionary<string, string> _customProperties; private static Dictionary<string, Dictionary<string, string>> _fieldsCustomProperties; private static void SetupCustomPropertyHashtables() { _customProperties = new Dictionary<string, string>(); _fieldsCustomProperties = new Dictionary<string, Dictionary<string, string>>(); Dictionary<string, string> fieldHashtable; fieldHashtable = new Dictionary<string, string>(); _fieldsCustomProperties.Add("CompanyCode", fieldHashtable); ...... Dictionary<string, string> fieldCustomProperties = CompanyEntity.FieldsCustomProperties["CompanyCode"]; string requiredCap = fieldCustomProperties["RequiredCap"];
读取自定义属性RequiredCap的值为true时,设置控件的CharachterCasing属性。
再来看业务实体的业务计算如何发生。示例代码如下所示
protected override void OnFieldValueChanged(object originalValue, IEntityField2 field) { base.OnFieldValueChanged(originalValue, field); switch ((CompanyFieldIndex)field.FieldIndex) { case CompanyFieldIndex.DriverAssembly: OnChangeDriverAssembly((string)originalValue); break; } } private void OnChangeDriverAssembly(string originalValue) { if (this.DriverAssembly == originalValue || String.IsNullOrEmpty(DriverAssembly)) return; this.DriverType = BaseCommon.GetProjectName(ModuleType.BusinessLogic, DriverAssembly); }
当我在界面中改变当前界面插件程序集时,它会为我自动读取这个程序集的类型信息,项目命名信息。要理解这种方式,需要先理解.NET开发中的数据绑定技术。数据源控件相当于一个桥梁,连接数据实体和界面控件,当给数据源控件赋值时,控件会读取数据实体的值,当界面中的控件值发生改变时,借助于数据源控件,自动把更改后的数据回写到数据实体中。所以,当数据实体中值发生改变后,我们可以注册相应的改变事件,作出业务逻辑处理,数据源控件会读取改变之后的数据实体值,呈现在界面上。几乎所有的业务逻辑是依照此方式编程,也实现了界面和逻辑分离。
界面和逻辑分离后,界面中的作用就是将控件绑定到数据源控件,再以Code Smith来生成数据读写接口:
public override EntityBase2 LoadEntity(string refNo) { IItemManager manager = ClientProxyFactory.CreateProxyInstance<IItemManager>(); ItemEntity customer = manager.GetItem(refNo); return customer; } public override void DeleteEntity(EntityBase2 entity) { ItemEntity user = (ItemEntity)entity; IItemManager manager = ClientProxyFactory.CreateProxyInstance<IItemManager>(); manager.DeleteItem(user); } public override void SaveEntity(EntityBase2 entity) { ItemEntity user = (ItemEntity)entity; IItemManager manager = ClientProxyFactory.CreateProxyInstance<IItemManager>(); manager.SaveItem(user); }
系统中所有与数据库读写相关的界面代码均是以此方式实现。
接口与它的实体均以Code Smith模板生成,效率高。如下所示的供应商接口
public interface IVendorManager { VendorEntity GetVendor(System.String VendorNo); VendorEntity GetVendor(System.String VendorNo, IPrefetchPath2 prefetchPath); VendorEntity GetVendor(System.String VendorNo, IPrefetchPath2 prefetchPath, ExcludeIncludeFieldsList fieldList); EntityCollection GetVendorCollection(IRelationPredicateBucket filterBucket); EntityCollection GetVendorCollection(IRelationPredicateBucket filterBucket, ISortExpression sortExpression); EntityCollection GetVendorCollection(IRelationPredicateBucket filterBucket, ISortExpression sortExpression, IPrefetchPath2 prefetchPath); EntityCollection GetVendorCollection(IRelationPredicateBucket filterBucket, ISortExpression sortExpression, IPrefetchPath2 prefetchPath, ExcludeIncludeFieldsList fieldList); VendorEntity SaveVendor(VendorEntity vendor); VendorEntity SaveVendor(VendorEntity vendor, EntityCollection entitiesToDelete); VendorEntity SaveVendor(VendorEntity vendor, EntityCollection entitiesToDelete, string seriesCode); void SaveCollection(EntityCollection vendors); void DeleteVendor(VendorEntity vendor); bool IsVendorExist(System.String VendorNo); bool IsVendorExist(IRelationPredicateBucket filterBucket); int GetVendorCount(IRelationPredicateBucket filterBucket); VendorEntity CloneVendor(System.String VendorNo); void PostVendor(System.String VendorNo); void PostVendor(VendorEntity vendor); void ApprovalItem(EntityCollection vendors); }
实现接口的Manager类型代码例子如下
public class VendorManager : Foundation.Common.ManagerBase, IVendorManager { public VendorEntity GetVendor(System.String VendorNo) { return GetVendor(VendorNo, null); } public VendorEntity GetVendor(System.String VendorNo, IPrefetchPath2 prefetchPath) { return GetVendor(VendorNo, prefetchPath, null); } public VendorEntity GetVendor(System.String VendorNo, IPrefetchPath2 prefetchPath, ExcludeIncludeFieldsList fieldList) { VendorEntity _Vendor = new VendorEntity(VendorNo); using (DataAccessAdapterBase adapter = GetCompanyDataAccessAdapter()) { bool found = adapter.FetchEntity(_Vendor, prefetchPath, null, fieldList); if (!found) throw new Foundation.Common.RecordNotFoundException("Invalid Vendor"); } return _Vendor; }
界面层中或是实体层,使用下面的接口来访问接口:
ICompanyManager _companyManager = ClientProxyFactory.CreateProxyInstance<ICompanyManager>(); CompanyEntity _company = _companyManager.GetCompany(“Kingston”)
如果没有采用分布式技术(.net Remoting,WCF),CreateProxyInstance方法直接返回ICompanyManager接口的实体类型的实例,供接口调用。如果有应用.net Remoting技术,则先以下面的方法产生服务器对象:客户端产生的实体对象会是一个远程代理,指向远程对象:
RemotingConfiguration.RegisterActivatedServiceType(type);
接口与实现分离的好处在这里体现的很明显,简单的切换部署模式(单机,分布式)不需要改变代码。
我的博客:http://blog.striveforfreedom.net
最近修改一C程序,在一个结构体里加入了几个新的字段,编译完一跑竟然出现段错误(segmentation fault)崩溃了。用gdb查看,引发崩溃的是一条这样的指令:mov register offset(%rsp)。
从引发崩溃的指令可以看出,崩溃的原因是访问了栈上的内存,然而通常来说访问栈上内存是不会导致段错误的,因为栈上内存不需要程序员手动管理,一般来说很难出错。猜测有可能是栈溢出了,需要证实这个想法。发生崩溃的机器是X86_64+Linux,用ulimit -s得知进程栈默认的soft limit是10MB,因为程序代码并没有调用setrlimit调整过栈的soft limit,于是需要证明出现段错误的进程栈大于10MB了,导致崩溃的地址可以从gdb中查看,如果知道栈的起始地址(栈底),两者之差就是栈的大小。但如何才能知道栈的起始地址呢?我们知道Linux有个proc文件系统,系统里每个进程在/proc下都有一个以进程ID命名的文件夹,/proc/PID下包含的都是进程ID为PID的进程的相关信息,比如说该进程对应的可执行文件路径/当前目录/打开的文件等。其中/proc/PID/maps包含了该进程所有虚拟区域的起始和结束地址,包括栈(即文件maps里最后一个字段为[stack]对应的那一行)。拿到了栈的起始地址之后,用起始地址减去引发崩溃的那条指令中访问的内存地址(即rsp加一个偏移),得到的值果然大于10MB了。至于栈溢出的原因,原有代码在栈上定义了一个结构体的数组,而我在结构体里面加了几个size比较大的字段,因此溢出了。找到原因,BUG修改起来就很简单了,要么在shell里修改栈默认的soft limit,要么在代码里调用setrlimit,要么在堆上分配内存。
为了说明这个BUG,我写一段测试代码作为例子,代码如下:
int main(int argc, char* argv[]) { const unsigned len = 10 * (1U << 20); char data[len]; data[0] = 'a'; return 0; }
编译完一运行,出乎意料的是,进程竟然没崩溃!这就非常奇怪了,因为我在main函数里定义了10MB大小的数组(并且访问了第一个元素,即最地址最小的那个),且不说环境变量所占空间,单这个数组加上C运行库调用序列所占空间就超过10MB了,而栈soft limit是10MB,按理说必然崩溃。然而实际却没有崩溃,一开始我怀疑代码被优化掉了,用objdump一看并没有优化掉,接着想了很久都没有头绪,最后终于想起去查看内核代码,看看内核到底是怎么处理栈溢出的。这包含两个方面,一是栈的soft limit是怎么读取出来的,二是内核怎么检查栈大小是否超过soft limit了。发生崩溃的机器上装的是CentOS 5.7,用uname -r得到的内核版本是2.6.18-308.el5,这个版本号跟官方内核版本号对应不上,因为感觉应该很接近2.6.18,于是就查看了官方内核2.6.18的代码(推荐下 lxr.linux.no ,查看某个版本内核代码很方便,不用去下载几十M的源码包了)。
读取资源限制(soft limit & hard limit)是由系统调用getrlimit完成的,getrlimit在内核中的入口是sys_getrlimit,代码如下:
asmlinkage long sys_getrlimit(unsigned int resource, struct rlimit __user *rlim) { if (resource >= RLIM_NLIMITS) return -EINVAL; else { struct rlimit value; task_lock(current->group_leader); value = current->signal->rlim[resource]; task_unlock(current->group_leader); return copy_to_user(rlim, &value, sizeof(*rlim)) ? -EFAULT : 0; } }
这个函数很简单,就是把当前进程的某种资源限制读出来,并复制到用户空间,没有发现什么问题。
对栈的大小限制的检查是在页面异常(page fault)处理中完成的,从页面异常入口page_fault开始,查看调用序列page_fault > do_page_fault > expand_stack > acct_stack_growth,在函数acct_stack_growth中发现了对栈大小限制进行检查的代码,如下(省略了跟我们这个例子无关的代码):
static int acct_stack_growth(struct vm_area_struct * vma, unsigned long size, unsigned long grow) { //... struct rlimit *rlim = current->signal->rlim; //... /* Stack limit test */ if (size > rlim[RLIMIT_STACK].rlim_cur) return -ENOMEM; //... }
其中,参数size是栈的起始地址减去当前引发页面异常的地址并按页大小向上对齐的,很显然,这里如果发现栈大小大于soft limit就返回错误,最终会给当前进程发送SIGSEGV信号,导致进程出现段错误崩溃。上面的内核代码说明我的想法是正确的,然而进程并未和我预料的那样崩溃,一想可能是内核版本不对,于是又查看了官方2.6.19版的代码,发现这两处的代码并没有改过。这就很奇怪了,过了一会我突然想到,机器上装的是CentOS,CentOS可能修改了这处的官方内核代码,于是我下载了和我系统对应的源码包kernel-2.6.18-308.el5.src.rpm,安装之后,发现该版本对应的官方内核版本是2.6.18.4,CentOS的修改过的代码放在一个patch文件kernel-2.6.18-redhat.patch里,运行patch之后,果然发现CentOS修改了acct_stack_growth函数,修改如下(该函数有多处修改,这里只列出了跟我们这个BUG相关的修改):
static int acct_stack_growth(struct vm_area_struct * vma, unsigned long size, unsigned long grow) { //... struct rlimit *rlim = current->signal->rlim; //... /* Stack limit test */ if (over_stack_limit(size)) return -ENOMEM; //... }
一比较就可以发现官方内核代码是直接比较size和栈的soft limit,而CentOS把这个比较放进了函数over_stack_limit里,再来看函数over_stack_limit:
static int over_stack_limit(unsigned long sz) { if (sz < EXEC_STACK_BIAS) return 0; return (sz - EXEC_STACK_BIAS) > current->signal->rlim[RLIMIT_STACK].rlim_cur; }
其中EXEC_STACK_BIAS是一个整型常量,定义如下:
#define EXEC_STACK_BIAS (2*1024*1024)
很显然,CentOS把栈大小限制从soft limit往上提高了2MB,如果栈大小超过栈的soft limit+EXEC_STACK_BIAS(在我们这个例子中为12MB)则说明栈溢出。到此真相大白,把上面的测试代码修改一下(把数组大小改为12MB),再一运行进程果然崩溃。
有时候接近问题的真相,但并没有发现问题的全部,就这个问题来说,如果我不写这篇文章,也就不会去写上面那段测试代码,就会想当然地认为在我的机器上栈的默认大小限制是10MB了。