生命周期的约束问题,可以从方法执行的前置条件切入进而展开讨论。
几日前与同行讨论到这样一个问题:
在应用开发中,在运行时允许一个Method被成功激活的前置条件有哪些方面?
讨论归纳成为如下三个方面,在此与大家分享,希望可以抛砖引玉,多收集一些素材:
1. 安全性约束
2. 参数的约束
- 参数上下文无关约束
- 参数上下文相关约束
安全性约束
众所周知,JavaEE规范中明确的定义了安全性模型。虽然国内大多项目并未采用该安全性模型,而是转而通过应用程序自身完成的安全性管理(尽管JavaEE规范中提到这存在存在隐患),但是不可否认安全性约束是企业应用的一个非常重要的基础方面。
在JavaEE 8以前,JavaEE框架提供的安全性服务以组件级别安全性为主。相比之下,在概念上其他的应用程序或者PaaS(平台即服务) 提供了更细致的安全性服务。比如:Force.com这种多租户系统,首先提供了Tenant级别的安全性;在一个Tenant内部,也包含数据级别安全性,FLS (Field Level Security) 以及 基于条件的安全性。虽然这些概念不完全跟Method访问前置条件相关,笔者也稍作介绍。
JavaEE标准的组件级别安全性约束
JavaEE定义了一组安全性相关的概念:
- 安全域(Realm),通过一种具体的认证和授权方式,兼容JAAS。比如Glassfish中提供的各种用户安全性身份管理的持久化手段:FileRealm, JdbcRealm以及自定义Realm等等,配合相应的LoginModule来完成容器(抽象的概念,对这个概念陌生的同学可以理解成为服务器)对访问者身份以及角色的获取手段。比如要在一个文件中存储用户名和密码,可以选择Glassfish提供的FileRealm;想要通过数据库来访问和存储用户的安全性身份信息,可以使用JdbcRealm;如果想自定义认证的算法以及存储用户名密码的方式,可以使用自定义的LoginModule配合自定义的Realm。具体实现需要参看相应的应用服务器开发文档有关安全性的部分。
- 实体(Principle),代表一个个人,公司或者一个登陆ID的概念。
- 用户,不需解释
- 用户组,不需解释
- 角色,容器直接使用角色作为依据完成组件的授权。经过登录模块以及安全域的共同作用,一个合法的用户会映射到一个包含多种角色的数组。
- 组件,受安全性控制的资源,相对组件框架而言,Servlet, JSP,EJB等等均是组件。一个url pattern可以指明一组受控资源;一个EJB的方法也可以是一个独立的受控资源。
图1 JavaEE安全性核心概念 摘自Websphere开发文档
当运行时一个客户端请求被送到服务器时,可能是对一个远程方法的访问,或者是对一个某一个网页的访问,再或者是对某一个RESTful Resource的访问,在访问到具体资源发生之前,服务器会检查所访问资源是否需要安全性控制。如果有访问控制要求,则需要鉴别当前请求是否具有资源访问所需要的角色,但是在鉴别角色之前,首先要认证发出请求用户的身份,这时登录模块就会发挥作用,会向用户索要客户的credential,可能是用户名密码,可能是证书等等。倘若经过登录模块以及安全域的共同协作使得用户通过了身份认证(鉴别)后,用户被赋予一定的角色。此时再回到开始角色匹配的阶段,倘若isCallerInRole,那么该远程方法,或者一个受限网页,或者一个RESTful的Resource可以被访问;否则容器直接拒绝激活上述可访问对象。以上是安全性约束对激活方法前的大概控制过程。进一步了解请参阅JavaEE Application Security Using Websphere
Force.com 安全性
Force.com作为一个有完整生态圈的云计算平台(PaaS)一直再演进。笔者接触到了如下安全性约束:
- 数据级别安全性:数据隔离。包括用户数据的隔离以及租户(Tenant)数据的隔离。
- FLS:Field Level Security 域级别访问控制,包括针对某Profile或者User是否可见,是否可写。
- 基于条件的安全性:数据隔离就会产生数据共享的需求,故在满足一定条件的情况下,数据对不同的用户的可访问性控制。
回到JavaEE对业务方法的安全性访问控制,运行时能否激活一个公有方法,首先需要过身份认证和鉴别这一关,对于采用标准JavaEE安全性模型的应用而言,这部分功能是由容器提供的,访问请求在发生授权的过程还没有接触到应用程序本身。可以参考@RolesAllowed相关的API等等。
参数的约束
抛开具体的规范、框架和架构,参数的约束包括上下文相关和上下文无关两部分,
参数的上下文无关约束
众所周知,对于BS应用程序,无论Client, Browser还是Server,都要进行参数自身有效性的校验,这部分也非常基础,比如一个字符串型参数的长度的校验或者一个邮件型字符串的合法性校验等。
JSR303 Bean Validation在Server Side模型层辅助完成了部分校验功能,例如有提供@NotNull之类的约束注释。请参阅IBM Developerworks之技术规范特性概述
图2. 模型层的Java Bean校验 摘自IBM DeveloperWorks
参数的上下文相关约束
JavaEE核心模式中给出了业务层架构的范例,其中定义了Application Service以及Business Object两个层次概念。大体上,为了解耦不同业务对象之间的依赖关系,引入了Applcation Service的概念。因为JavaEE出现本来目的是为了兴起业务组件的市场。不同的厂商提供不同领域的Business Object的组件,共同部署到一个产品环境或者同一个应用服务器。那么为了解决不同厂商提供的Business Object的互操作或者依赖问题,引入了Application Service的概念,用来集成不同厂商提供的业务模块。然而现实并非完全如专家组所预期,有不少项目通常是一票到底,若开发设计实践(未能合理的抽象)未着重于断绝不同业务对象之间的依赖,那么这将导致整个应用过于复杂,尽管有服务层也未起到预期的作用。这都是题外话,为了介绍Application Service和Business Object的概念引入的。
在Application Service层次,参数校验需要结合当前上下文来校验当前请求是否有效。经过一些应用逻辑后,要么激活相关的业务对象方法;要么拒绝请求而未激活业务对象的方法。
在Business Object层次也有类似的情况。
这类约束需要应用服务开发人员自己开发完成。
生命周期约束
业务对象不同于基础代码对象,包含明确的生命周期特征以及业务逻辑。并且业务对象的业务方法的激活通常是有一定的时序要求。在软件建模技术中,这部分由UML的状态图来刻画一个对象的生命周期。在实际开发中会常见跟生命周期相关的一些情况。如图3,对于一个未打开的网络连接进行接收或者发送的行为都是没有意义的;而对于一个已经打开的网络连接执行connect操作也是没有意义的。这种时序性要求本质上源于生命周期约束。
图3 连接的生命周期示意图
由于相同模块的业务对象之间存在关系:依赖,关联,聚合以及组合。故一个业务对象的业务方法除了业务对象自身的生命周期约束外,还可能包括关系对象的生命周期约束。比如一个生产任务的调度行为依赖于所需生产线资源是否空闲可用。生产任务有独立的生命周期,生产线资源有独立的生命周期。生产任务的调度行为依赖于空闲可用的生产线资源。显而易见,倘若为生产任务所指定的生产线资源处于繁忙状态时,对该生产任务的调度行为是无效的。这是“依赖”关系的生命周期约束。
图4 合约型按订单生产配送示例类图
例如图四,一个根据合约型根据客户订单生产的系统,每一个客户跟生产厂商签订一个长期有效的合同。在合同期内,客户向生产厂商下特定的订单,生产厂商根据合同中的收费标准以及订单中的项目向客户收费,并再生产完成后将产品通过配送部门运输给指定的收货方。
在大环境资金都相对紧张的条件下,倘若在生产环节中,某客户被识别出不具备支付能力,生产厂商有权利终止对该客户相关的一切生产行为以及配送行为。又或者由于供应商普遍提高供货价格,而导致产品生产成本增加,此时厂商有权利重新与客户再次协商价格。在新的价格妥协之前,应该停止生产活动。
为了简单起见,假设订单的生命周期如图5所示,而生产任务的生命周期如图6所示。
图5 订单的生命周期
图6 生产任务的生命周期
要满足这样的需求,要求生产任务的状态在进行转移之前首先要校验自身状态的有效性。即自身的状态必须在“父亲”业务对象(合约)生命周期的特定阶段。比如,生产任务的状态BOMConfirmed有效的前提条件是生产任务的“父亲”对象(合约)应该在Active状态。倘若合约对象当前处于Aborted状态,那么BOMConfirmed状态则是一个无效的状态。生产部门不应该在某合约对象处于Aborted状态时而进行任何相关的生产活动(可能存在为撤销生产而进行的活动)。
当需要获取合约的有效状态时,与上述过程相同,“合约”对象也要参考其父亲业务对象“客户”是否处在“活动”状态。显然,这是一个递归的过程。
上面讨论的涉及到了一种“聚合”关系的生命周期约束。
对于实现这些约束,到底由方法本身来进行判断或者在接入方法之前进行判断,这属于实现层面的选择。但若选择通过方法本身进行判断,则这种层级关系的递归式生命周期约束检查会使得应用代码显得的繁琐的同时也还存在一些其他的问题。如对象本身的生命周期的设计可能与代码实现不符;对象关系的生命周期约束与代码实现不符甚至完全被忽略;声明周期出现变化时需要修改业务对象本身以及相关联的对象方法大量代码,这往往使得对象的生命周期很难发生变化而且为了满足功能还要添加一些hack性质的代码修改。
笔者的经历是设计者或者程序员往往忽略了生命周期约束的检查。某硅谷一个著名软件服务公司上线运行了6年的软件服务,也部分的忽略了关系生命周期约束检查,导致某一个服务经过多次讨论,修改,上线,回滚,最终放弃了提供该服务。这使得在一些边界状态下,系统会产生令客户愤怒的行为。当然这个问题本身很简单,但是疏漏的设计导致现实变得复杂了。对于多层级业务对象而言,如果一开始能设计好生命周期约束,那么解决这个问题是相当容易的事情。
在现实的开发设计实践中,不仅仅设计可能会产生疏漏,就算是设计未能产生疏漏,编码也可能与设计产生不一致。因为对于大多数的项目或者产品或者服务而言,正向测试是基础,而负向测试可能永远是不够的。尤其是跨越层级的生命周期变化相关的负向测试,往往会被忽视或者由于测试成本不足而忽略。即便不被忽略,那么无论从开发上或者是测试上,这方面的约束检查都会有较大的开销。
不仅如此,对于一个经历着数年考验的系统来说,经历过N次功能代码的修改后,一个业务对象自身的准确的生命周期已经不能被准确的刻画或者描述了,过去的设计文档早已过时。随着某个业务对象自身生命周期的变化,对依赖到这个业务对象的那些业务对象也产生了影响。如何保证某个具体的生命周期状态的语义发生变化后,原来可以通过的测试仍然是正确的行为呢?任何生命周期相关的代码修改和增强都可能导致某些方法可能出现漏洞,再加上系统测试往往不能涵盖所有负向的生命周期约束检查逻辑,最终使得这些漏洞暴漏给用户。
此外,对于一个多租户的系统,不同租户的相同业务对象是否可以有不同的生命周期呢?这个时候应用程序该如何写代码来校验生命周期约束呢?这些都是值得思考的问题。
至此,我们试图归纳总结这些生命周期约束的相关问题:
- 业务对象当前所处状态的有效性检查,比如前例提到生产任务单在“在流水线”状态准备切换至“确定下线”状态时,通过关系的生命周期约束,首先校验“在流水线”状态是否有效。当检测到客户或者合同或者订单等状态发生变化时,系统认为“在流水线”状态无效,不可以完成一般意义的确认下线操作,系统需要根据当前的状态,给出相应的提示信息。
- 业务方法的有效性约束检查,比如对于一个“已下线”的订单,不可以在进行重新排产操作
- 依赖关系对象生命周期检查,要求当业务对象进入某个状态时,其依赖对象必须处在某个状态,否则业务方法不可被激活
- 并发条件下业务对象或者被依赖的业务对象的状态变化需要的读写锁控制
- 关闭公有的生命周期状态的设定方法,对应用程序员隐藏对业务对象生命周期状态的设定
- 分离生命周期事件触发逻辑(包括生命周期事件和生命周期回调)与业务逻辑,以使得业务逻辑更加简洁。
- 不需要应用程序员或者测试人员编写生命周期约束检查的繁琐逻辑以及繁重的反向测试用例。
这类约束检查,过去没有第三方支持,需要应用程序员开发。现如今包含生命周期相关约束检查的一种元驱动形式的第三方开源项目出现了。
应用程序员能够通过简洁的元数据(直接表达语义)来描述一个业务对象自身的状态转换图(即其生命周期),以及业务对象与所关系对象之间的生命周期约束。在运行时生命周期引擎会完成全部上述工作,而应用程序员不需要调用任何生命周期框架的API。
综上,在方法执行的前置条件检查的三个方面,大部分都有开源软件类库或者开源软件框架提供相对应的支持。
- 安全性约束方面,JavaEE框架提供了可扩展的基于组件的安全性模型
- 参数约束方面
- 上下文无关参数校验,JSR303 Bean Validation提供了可扩展的支持
- 上下文相关参数校验,需要应用程序员完成
- 生命周期约束方面,Lifecycle框架提供了较完整的元驱动形式的编程模型
认识到这些方面的问题,理解其中的思想,适当的开源软件类库以及框架,可以大幅减少开发工作量,提高代码质量,增强应用的安全性,代码的健壮性,可重用性,扩展性以及可维护性。
在此分享该开源项目Lifecycle (on github.com)
样例代码: