Tag: framework

Beyond OSworkflow

最近结束了一个企业OA系统的项目,客户是一家海洋航运行业的企业,散运业务全球第一。该系统以工作流系统为基础平台,对员工工作进行电子化和规范化,由系统来驱动员工自动、快捷、可管地完成日常工作。在开发过程中,在综合评比多种工作流产品之后,我们选择了开源工作流产品——OSworkflow作为底层工作流引擎支撑,通过扩展OSworkflow的接口把业务系统和工作流引擎完美无缝地集成在一起。本文就是跟大家一起分享osworkflow扩展过程中的经验心得,希望能对其他朋友有帮助。

在我们开发团队介入系统开发过程之前,这个项目已经完成了 quickstart 阶段,留给我们的阶段产品有:系统实现的proposal、一个能体现用户page flow的lo-fi,以及在mingle上的 story list和iteration原始计划。当然,还有项目团队跟客户形成的一个良好的互信基础和沟通渠道。于是,左 proposal,右 lo-fi,客户沟通在中间,我们就开始了系统设计的 inception。inception阶段的目标根据不同项目会有不同。在这个系统里面,我们完成的目标有:确定系统实现使用的技术、针对技术进行spike探讨可行性、结合master story以及比较粗略的story list实现一个系统prototype。好,工作开始了!

为什么选择OSworkflow?
工作流系统其实在理论界已经是研究得非常成熟了,有 WfMC 组织来规范工作流定义语言,而且各种工作流引擎,不仅商业产品,像 JBPM、OSworkflow、ofbiz等开源产品也都很多。这些产品都各有特点,应该如何选择呢?从使用率和文档完整度来看,JBPM 和 OSworkflow 占了上风,我们就从这两者里面选了。JBPM基于UML的状态图和活动图来定义流程,已经加入JBOSS大家庭,但是相对比较重。osworkflow是个非常轻量级的工作流产品,但是自从2006年就停止活动了。不管如何,选择的技术都是要为解决未来系统开发会面临的问题,才算是一个好的可行的选择。

通过分析quickstart阶段的proposal和lo-fi prototype,然后再反复跟客户确认,我们知道了客户在现阶段对系统的要求是:

  1. 只需要处理线性的工作流(未来可能需要增加对分支/合并的支持)
  2. 不需要功能复杂强大的工作流编辑器
  3. 节点流转主要是手工触发,不需要基于规则
  4. 需要能和遗留系统已有的工作流定义集成,而已有工作流定义用 osworkflow 定义

根据客户对系统的要求,我们选择了osworkflow作为最终的候选工作流引擎。因为我们对这几种工作流产品都不是很熟,对它们是否能满足系统要求答案不确定。而从另外一个方面来讲,如果系统足够简单,我们也可以自己实现一个状态机,自己实现对流程节点流转的维护。

为了做出选择,我们又分头对 osworkflow 的配置文件以及技术架构都进行了细致的研究。研究过程和结果就不细说了,网上有很多 OSworkflow 的介绍文章,大家可以参照。OSworkflow 会把一个简单的xml配置文件,转换成它定义的流程描述类(XXXDescriptor,我们也把它们称为osworkflow的models),然后在流转的过程中,将其转换成工作流实例和工作流节点,并通过相应的持久化接口持久化工作流的状态。整个过程用一个简图来表示,就会是:

图1 OSworkflow model

考虑到 OSworkflow 的实现也是非常轻量级,并且结构非常清晰,而且为了提供以后对分支/合并、以及自定义规则等流转形式的支持,我们还是选择了 OSworkflow,而不是自己去实现一个基于状态机的工作流引擎。但是,我们还不能直接把 OSworkflow 放进项目里面,然后直接引用:一方面我们不想让开发团队都纠缠于底层 OSworkflow 的技术细节,另外一方面也是因为 OSworkflow 并不能完全满足客户的系统要求。我们需要扩展!

为什么去扩展?
依然是问答模式,OSworkflow 有哪些方面不能满足我们系统的要求?

1. 系统对各个流程的定义都能有版本跟踪

客户会对流程定义进行修改,但是流程修改的过程中,流程应该继续按照原来的定义进行流转。这样,对于同一份流程,系统会同时存在两个不同的定义,并且都能正常流转。OSworkflow 基于XML文件的流程配置方式在这种情况下就不能满足。

2. 系统对流程定义的修改能实现“hot deploy”

客户对流程定义进行修改后,系统能自动提供给用户新的流程定义,而不需要重启服务器。因为 OSworkflow 是在系统启动的时候,将所有的流程定义都载入内存,在系统运行期不会去检测流程定义的修改。这样,OSworkflow 也不能满足。

3. 相对于系统的要求,OSworkflow 提供的功能还是太复杂了

因为 OSworkflow 是提供一个通用的工作流引擎,所以考虑了很多复杂的情况。这些复杂性对我们系统来讲,都是额外的不能对客户产生价值的特性,而且那些复杂性的存在,也会让我们开发的时候必须考虑到那些方面,从而影响我们的模型设计和开发。

除了上文提到的几个原因,我们考虑的因素还有很多方面,最终我们决定是在OSworkflow基础上进行扩展,来满足我们的要求。

如何进行扩展?

如何进行扩展?我们的原则是什么?没有一个清晰的目标,很可能最终扩展的方向和最终实现就会跟我们想要的目标离得比较远了。

原则一. DDD,MDA

我们希望开发团队在设计开发的过程中只需要考虑系统领域的模型,而不会 involve 到底层的流程流转细节。

“find core domain models”,我们重新研究了系统的 proposal 和 lo-fi prototype,找出系统里面的领域对象,它们之间存在这样的一个关系:

图2 系统model
从图1 OSworkflow model 关系图和图2 系统 model 关系图可以看出,它们两者还是存在着一定的相似点和不同点的。

相同点:
1. 相应 model 完成的功能和职责是非常类似的
2. 不同 model 之见的关系也是非常类似的

不同点:
1. model 的属性
2. model 的一些行为

原则二. decoupling

我们希望项目实现不依赖于某个工作流产品,而是可以很方便地迁移到其他的工作流产品。第一想法就是——“laying, isolate by interface”。

其实工作流产品除了工作流引擎负责流程节点流转之外,都另外包括这三部分:流程定义、定义解析和状态持久化。而成熟的工作流产品,不管是JBPM,还是OSworkflow,都会提供接口进行隔离。在OSworkflow里面,接口WorkflowFactory会负责流程定义的解析和OSworkflow models的初始化,接口WorkflowStore则负责将Workflow和WorkflowEntry的状态持久化。这样,我们就可以定制实现接口,将domain models转换成 OSworkflow models,在流程流转需要持久化流程状态的时候,则会去持久化domain models。

经过这样几个方面的论证和分析,得到的最终方案如下:

  1. 使用domain model,而不是工作流model
  2. 扩展接口将 domain model 转换成工作流 model
  3. 扩展接口提供自定义的工作流状态持久化
  4. 不改动 osworkflow 内核

最终实现

图3 系统实现图

我们得到了什么

  1. 使用 domain model 进行开发
  2. 底层工作流引擎对开发团队透明
  3. 设计、优化工作在应用层次来进行

(本博未来会以同一个项目为例对敏捷项目各个阶段过程进行简单的介绍,敬请期待)

看上去很美 –OOafarian对play!框架的几点看法

 play! framework 是一个面向小型网站开发的 rails-like 的 Java 框架,不仅在目录结构上,在系统 skeleton 生成上,也把 rails 学了个七七八八。最近和同事也在做一个 rails style 的 Java Web 应用开发框架,参考了 rails 的很多 feature,但更多的是按照自己的开发理念和哲学思想“拿来” rails 里面有借鉴意义的思想。面对号称 rails-like 的 play! framework,这几天身在海滩上,于是花了一些时间好好研究了 play! 的源代码一番。“看上去很美”,是读完代码后脑海中第一下涌现出来的想法,我承认我是OOafarian。
 
 play! framework 5个“酷”的东西,看上去很美,但这些大都是建立在对 Java class 文件 hack 的基础上的。以 JPAModel 为例,任何它的子类 model 都是可以使用 rails style 的语法:User.findAll()。表面上看和 activerecord 的语法一样的,但实际上 play! 是通过在 JPAModel 里面定义这些静态方法,默认实现是抛出 RuntimeException,然后在 classloader 载入子类 class 文件的时候,通过增强每个 model class,使之具有这样类型安全的类方法。且不说这样实现是多么暴力和对开发人员不可理解,这与 rails 的实现是完全不一样的。ruby 因为是提供了 class 的继承体系,使得类方法也成为可以 override 的 class 类的实例方法,但是 Java 不提供 class 的继承和覆盖:这是 Java 的优势也是劣势。有人说这是语言的天性,但其实语言也是人们对世界进行抽象建模的一种折射,也反映的是人们的世界观和哲学思想。如果是我,如果依旧选择 Java 来做,我更倾向于保留 Java 的静态方法的优势和优雅,通过模式或者设计方法使之具有类似于动态语言的灵活性。这也是我和几位同事一致的看法。
 
 那么,从一位受 OO 和 pattern 熏陶很深的 Java 程序员看来,看上去很美的 play! framework 华丽的袍子下有多少虱子呢?待我细细数来:
 1. controller 里面充斥的 static void 方法,不 OO,不 testable,完全是过程化的代码。作者也是说了:
 
 The Java code from the Controller class isn’t really Object Oriented : it’s mainly some procedural code.
 
 当然,过程化还是面向对象,这个争论不是今天才有,不同的人在不同的上下文情景下都会选择最适合自己的开发方式。但是,作为受 OO 熏陶很深的我,这是不可接受的。
 
 2. view 的 render 实现是继承自 RuntimeException。记得很早之前,还有人争论 Exception 作为程序出口和条件判断分支的使用,但自《Effective Java》一书以及其他编码风格介绍的书籍出来之后,这样的争论早已不知何处。Exception 和 Error 会有自己适合的场所,也有它们自己的正确含义,被滥用可不是它们的错。我想,即使是 JVM 被优化得性能完全不用考虑了,这样的处理思路也是不可以被团队成员理解、认同和接受的吧?
 
 3. 暗地里修改代码,使用自己的classloader来大量增强或者修改class,甚至反编译 class。这样就导致很多增强的东西都是框架来强制性加入的,开发人员没有办法进行测试,也没有办法修改默认的实现。也许有人会觉得这样也挺好,觉得这样也够用,那也 OK。但是如果这些都建立在抛弃 Java 强大的静态类型安全,强大的 IDE 支持,强大的社区支持的基础之上,我是不能接受。
 
 当然,play! framework 也是体现了一些很新颖的创新。比如使用 Http Server 来取代 J2EE Server,甚至是 Application Server。又比如通过动态编译代码来实现 hot swap。还比如使用 java.lang.instruments API 来定位 Exception 的位置。这些创新在一定程度上减轻了 Java web 应用开发过程中的“重”,给开发人员带来“轻”的感觉。但是,如果这些需要付出 Java 社区积累到几天的成果,完全重头再来,我想我会宁愿选择 ruby on rails。
 
 当我们选择了 Java,我们看重的是强大的静态安全,看重的是强大的 IDE 支持和社区支持。虽然这些会给开发过程带来一定的“重”,但是米兰昆德拉说过,
 
    可是,沉重便真的悲惨,而轻松便真的辉煌吗?
    最沉重的负担压得我们崩塌了,沉没了,将我们钉在地上。可是在每一个时代的爱情诗篇里,女人总渴望压在男人的身躯之下。也许最沉重的负担同时也是一种生活最为充实的象征,负担越沉,我们的生活也就越贴近大地,越趋近真切和实在。
    相反,完全没有负担,人变得比大气还轻,会高高地飞起,离别大地亦即离别真实的生活。他将变得似真非真,运动自由而毫无意义。

 rails 之所以能轻,是因为它有 runit,有 rspec 来作为它的安全网,使之不会偏离方向。而 Java 也可以变得很轻,但这个轻却绝不是以抛弃 Java 的优势作为代价的。这也是我和几位同事在做 rails style 的 Java web 应用开发框架的过程中一直坚持的,希望到时能展现给大家一个高效率而有符合编程哲学的好框架。

play! framework hot swap 浅析

play! 最大的卖点就在于 hot swap,正如它自己宣称的:
reach your maximum productivity。play! 允许开发人员修改java文件,保存,然后刷新浏览器,立马可以看到效果。不需要编译,也不需要重启服务器。
Java 要想实现动态更新 class 文件,不外乎两种手段:替换 classloader、替换 JVM。因为替换 JVM 引起的开销更大,需要维护 JVM 的堆、栈等运行信息,所以 hot swap 通常是选择替换 classloader。比如 grails 里面就是选择替换 classloader,它会自己维护一个线程,定期轮询源文件是否发生修改,以替换原来的 classloader。那么 play! 宣称的 hot swap 又是怎么实现的呢?
让我们来看看play! 的内部流程:
1. play! 使用了 Apache Mina 作为底层的 http server,然后使用了自己关于 Mina IoHandler 接口的实现—— HttpHandler
2. 当浏览器发起一个 request:
2.1 Mina Server 生成一个 Mina Request,转发给 HttpHandler 的 messageReceived 方法
2.2 play! 解析 Mina Request 和 Mina Session,包装成自己的 Request 对象

Request request = parseRequest(minaRequest, session);

2.3 play! 检测 Route 文件修改情况,根据 Route 配置信息将 Route/Action 的信息赋给 Request 对象

Router.detectChanges();
Router.route(request);

2.4 play! 根据当前配置的开发模式来采用不同的策略调用 Action 来理 Request

if (Play.mode == Play.Mode.DEV) {
Invoker.invokeInThread(
new MinaInvocation(session, minaRequest, minaResponse, request, response));
}
else {
Invoker.invoke(
new MinaInvocation(session, minaRequest, minaResponse, request, response));
}

2.5 如果 play! 当前是 DEV 模式,invokeInThread方法会让 invocation 对象代理 run() 方法

public void run() {
try {
before();
execute();
after();
}
catch (Throwable e) {
onException(e);
}
finally {
_finally();
}
}

咱们来看看 before() 方法:

public static void before() {
Thread.currentThread().setContextClassLoader(Play.classloader);
if(!Play.id.equals(test)) {
Play.detectChanges();
if (!Play.started) {
Play.start();
}
}
//
}

在 Play 类的 detectChanges() 方法里面,有这么一句:


classloader.detectChanges();

哈哈,play! 修改源文件后,刷新浏览器即见效的奥秘就在这里了。再进去看看 play! 自定义 classloader 的 detectChanges() 方法:

public void detectChanges() {
// Now check for file modification
List<ApplicationClass> modifieds = new ArrayList<ApplicationClass>();
for (ApplicationClass applicationClass : Play.classes.all()) {
if (applicationClass.timestamp < applicationClass.javaFile.lastModified()) {
applicationClass.refresh();
modifieds.add(applicationClass);
}
}
List
<ClassDefinition> newDefinitions = new ArrayList<ClassDefinition>();
Map
<Class, Integer> annotationsHashes = new HashMap<Class, Integer>();
for (ApplicationClass applicationClass : modifieds) {
annotationsHashes.put(applicationClass.javaClass, computeAnnotationsHash(applicationClass.javaClass));
if (applicationClass.compile() == null) {
Play.classes.classes.remove(applicationClass.name);
}
else {
applicationClass.enhance();
BytecodeCache.cacheBytecode(applicationClass.enhancedByteCode, applicationClass.name, applicationClass.javaSource);
newDefinitions.add(
new ClassDefinition(applicationClass.javaClass, applicationClass.enhancedByteCode));
}
}
try {
HotswapAgent.reload(newDefinitions.toArray(
new ClassDefinition[newDefinitions.size()]));
}
catch (ClassNotFoundException e) {
throw new UnexpectedException(e);
}
catch (UnmodifiableClassException e) {
throw new UnexpectedException(e);
}
// Check new annotations
for (Class clazz : annotationsHashes.keySet()) {
if (annotationsHashes.get(clazz) != computeAnnotationsHash(clazz)) {
throw new RuntimeException(Annotations change !);
}
}
// Now check if there is new classes or removed classes
int hash = computePathHash();
if (hash != this.pathHash) {
// Remove class for deleted files !!
for (ApplicationClass applicationClass : Play.classes.all()) {
if (!applicationClass.javaFile.exists()) {
Play.classes.classes.remove(applicationClass.name);
}
if(applicationClass.name.contains($)) {
Play.classes.classes.remove(applicationClass.name);
}
}
throw new RuntimeException(Path has changed);
}
}

HotswapAgent类的 reload 方法如下:


public static void reload(ClassDefinition definitions) throws UnmodifiableClassException, ClassNotFoundException {
instrumentation.redefineClasses(definitions);
}

读到这里,也就弄清楚了 play! 怎么实现 hot swap 的原理了,还是调用java.lang.instrument目录下的类和方法来实现的 hot swap。不存在魔法,play! 还是选择了替换 classloader,只不过这个替换动作发生在处理 http request 的时候,于是开发人员用起来就是“刷新浏览器就可以看见效果了”。