Morning@Weblog

11/30/2004

OSWorkflow解读之九

Filed under: — site admin @ 9:08 am

UserManager>>

OSWorkflow在用户管理方面所提供的功能,主要包括用户的创建、群组的定义、用户验证、以及对step执行人的跟踪记录和执行权限的判断等等。

用户/群组的管理是由UserManager来完成的,它包含于一个单独的jar包内。我们可以这样使用UserManager:

UserManager um = UserManager.getInstance();
User test = um.createUser("test");
test.setPassword("test");
Group foos = um.createGroup("foos");
test.addToGroup(foos);

利用UserManager也可以实现用户验证功能:

UserManager um = UserManager.getInstance();
boolean authenticated = false;
authenticated = um.getUser(username).authenticate(password);
if (authenticated) {
 session.setAttribute("username");
 ……
} else {
 ……
}

关于step执行人的跟踪,首先我们可以在创建流程的时候传入调用者(caller)名称,比如:

Workflow wf = new BasicWorkflow((String) session.getAttribute("username"));

BasicWorkflow会负责创建一个实现了WorkflowContext接口的实例,其中记录了caller的信息。利用com.opensymphony.workflow.util.Caller,可以将WorkflowContext中的caller随时植入transientVars中,以供后续的条件判断。为此,我们需要在流程定义文件中的适当位置加入如下定义(比如initial-actions中的pre-functions节点处):

<pre-functions>
 <function type="class">
  <arg name="class.name">com.opensymphony.workflow.util.Caller</arg>
 </function>
</pre-functions>

Caller是一个FunctionProvider,其excute方法中包含了如下代码:

WorkflowContext context = (WorkflowContext) transientVars.get("context");
transientVars.put("caller", context.getCaller());

同时,我们还可以指定流程中某个step的执行人(owner),只需要在action的results节点处为其指定owner属性:

<step id="2″ name="Edit Doc">
 <actions>
  <action id="2″ name="Sign Up For Editing">
   ……
   <results>
    <unconditional-result old-status="Finished” status="Underway” step="2″ owner="${caller}"/>
   </results>

利用caller和owner信息,我们可以在流程定义文件的condition节点中以多种形式指定限定条件,比如,利用脚本限定只允许caller为test的用户触发某结果:

<result old-status="Finished">
 <condition type="beanshell">
  <arg name="script">
   propertySet.getString("caller").equals("test")
  </arg>
 </condition>
 ……
</result>

又比如,利用util包中的OSUserGroupCondition限定仅当caller为foos群组中的用户时,才触发action:

<action id="1″ name="Start Workflow">
 ……
 <condition type="class">
  <arg name="class.name">com.opensymphony.workflow.util.OSUserGroupCondition</arg>
  <arg name="group">foos</arg>
 </condition>

再比如:利用util包中的AllowOwnerOnlyCondition限定仅当caller等于owner时,才触发action:

<action id="1″ name="Start Workflow">
 ……
 <condition type="class">
  <arg name="class.name">com.opensymphony.workflow.util.AllowOwnerOnlyCondition</arg>
 </condition>

11/29/2004

OSWorkflow解读之八

Filed under: — site admin @ 10:21 am

WorkflowQuery及其有关查询类>>

我们知道,通常人们总是希望了解流程当前的运行状况,因此就需要工作流引擎在提供流程流转的基本功能的同时,还需要提供查询功能。在osworkflow中,查询功能是由WorkflowQuery及其相关类提供的。

WorkflowQuery提供了两种类型的构造函数:

public WorkflowQuery(int field, int type, int operator, Object value)
public WorkflowQuery(WorkflowQuery left, int operator, WorkflowQuery right)

我们可以利用第一个构造函数创建基本的WorkflowQuery实例,然后利用第二个构造函数组织装配。以查询执行者是“test”且状态是“Underway”的step实例为例:

WorkflowQuery queryLeft = new WorkflowQuery(
  WorkflowQuery.OWNER, WorkflowQuery.CURRENT, WorkflowQuery.EQUALS, “test");
WorkflowQuery queryRight = new WorkflowQuery(
  WorkflowQuery.STATUS, WorkflowQuery.CURRENT, WorkflowQuery.EQUALS, “Underway");
WorkflowQuery query = new WorkflowQuery(
  queryLeft, WorkflowQuery.AND, queryRight);
List workflows = wf.query(query);
for (Iterator iterator = workflows.iterator(); iterator.hasNext();)
  Long wfId = (Long) iterator.next();
}

装配好的查询条件,将会传入AbstractoWorkflow的query方法中,然后再由WorkflowStore的query方法执行具体查询操作。不同的WorkflowStore实例其查询方式不尽相同,以MemoryWorkflowStore为例,它将遍历所有位于cache中的流程,然后将满足条件的流程ID放入一个ArrayList中返回,查询的核心代码采用了递归调用的形式:

if (query.getLeft() == null) {
 return queryBasic(entryId, query);
} else {
 int operator = query.getOperator();
 WorkflowQuery left = query.getLeft();
 WorkflowQuery right = query.getRight();
 switch (operator) {
  case WorkflowQuery.AND:
   return query(entryId, left) && query(entryId, right);
  case WorkflowQuery.OR:
   return query(entryId, left) || query(entryId, right);
  case WorkflowQuery.XOR:
   return query(entryId, left) ^ query(entryId, right);
 }
}

这里的queryBasic再次使用了递归调用的方式,详细情况可以查看osworkflow的源代码。通过这样的方式,可以满足任意复杂的流程查询条件指定。

11/28/2004

OSWorkflow解读之七

Filed under: — site admin @ 4:32 pm

Descriptors>>

在osworkflow的很多地方都会看到Descriptor的使用,最重要的一个是前面提到的WorkflowDescriptor。除此以外还有,ActionDescriptor、ConditionsDescriptor、ConditionDescriptor、FunctionDescriptor、PermissionDescriptor等等。这些类均位于com.opensymphony.workflow.loader包中。它们的作用,除了提供getter方法外(类似model的角色),还负责xml文件的读取与写入。WorkflowDescriptor在执行xml文件的读写时,如果涉及具体的流程定义元素,将会交由对应的Descriptor类来完成。比如,在WorkflowDescriptor的writeXML方法中,对initial action的序列化是这么实现的:

XMLUtil.printIndent(out, indent++);
out.println("<initial-actions>");
for (int i = 0; i < initialActions.size(); i++) {
 ActionDescriptor action = (ActionDescriptor) initialActions.get(i);
 action.writeXML(out, indent);
}
XMLUtil.printIndent(out, –indent);
out.println("</initial-actions>");

实际上,这是典型的Interpreter Pattern的运用,下面是类图总结。

11/27/2004

OSWorkflow解读之六

Filed under: — site admin @ 10:53 am

pre function和post function>>

pre function和post function是osworkflow提供的又一特色,它为某项执行逻辑提供了前驱和后继处理,运用十分灵活。并且,osworkflow为许多元件的执行逻辑都配备了pre function和post function的调用时机。这一点也可以从AbstractWorkflow.doAction的执行逻辑中看到。可以使用和pre function和post function的元件包括:action,result/unconditional result,step,split,join。

ScriptVariableParser>>

作为osworkflow的一个util class,ScriptVariableParser的主要功能是将给定字串中的${var}替换成相应的value。这意味着我们可以在许多地方使用类似于Ant中引用property的语法,来进一步提高灵活性。比如:
<results>
 <unconditional -result old-status="Finished” status="Underway” step="1″ owner="${caller}"/>
</results>
在这里,unconditional result的owner属性将被caller的实际值所替代。

TransientVars和PropertySet>>

在osworkflow的流程流转过程中,时常会用到Transient Vars和Property Set。这两个工具用来暂存一些临时信息或者在step间传递一些共享信息,比如:context信息,workflow entry信息,以及上面提到的${var}的value,等等。

Transient Vars实际上就是一个普通的Map,至于Property Set,则是opensymphony的另一个独立模块,需要单独下载jar包。与Transient Vars将信息暂存与内存不同,Property Set还支持数据库存储(JDBCPropertySet)

Register>>

为了更进一步提高灵活性,osworkflow还提供了Register功能。我们可以定义自己的Register,以执行特殊任务,并在流程定义文件中注明,该Register便会被动态注册到Transient Vars中,以备随时取用。以下便是一个典型的使用场景:

<registers>
 <register type="class” variable-name="log">
  <arg name="class.name">com.opensymphony.workflow.util.LogRegister</arg>
  <arg name="addInstanceId">true</arg>
 </register>
</registers>
<function type="beanshell” name="bsh.function">
 <arg name="script">transientVars.get("log").info("function called");</arg>
</function>

此外,为了方便使用,osworkflow的util包中还预定义了大量的Condition和FunctionProvider,以及其他的一些辅助类,比如:StatusCondition、AllowOwnerOnlyCondition、BeanShellCondition、Caller、EJBInvoker、ScheduleJob。

借助这些设施,osworkflow的扩展性、灵活性、易用性,得到了极大的体现。

11/26/2004

OSWorkflow解读之五

Filed under: — site admin @ 12:00 am

Schedule>>

在osworkflow中提供了定时执行某项任务的功能,这主要得益于opensymphony的另一个项目——Quatz,该项目为工作的定期执行提供了底层设施支持。为此,我们需要引用quatrz.jar包,好在osworkflow的下载包中已经包含了该文件。

为了实现任务定时,我们需要在流程定义文件中做类似如下的配置:

<function type="class">
 <arg name="class.name">com.opensymphony.workflow.util.ScheduleJob</arg>
 <arg name="triggerId">1</arg>
 <arg name="jobName">testJob</arg>
 <arg name="triggerName">testTrigger</arg>
 <arg name="groupName">test</arg>
 <arg name="repeat">10</arg>
 <arg name="repeatDelay">2000</arg>
 <arg name="cronExpression">0,5,10,15,20,25,30,35,40,45,50,55 * * * * ?</arg>
 <arg name="username">test</arg>
 <arg name="password">test</arg>
 <arg name="local">true</arg>
 <arg name="schedulerStart">true</arg>
</function>

ScheduleJob是一个FunctionProiver,因此具有execute方法。在该方法执行期间,ScheduleJob将会读取这些配置参数,创建好job实例(实际上是一个JobDetail实例)和trigger实例,然后启动schedule。大致流程如下:
- 根据传入的shedulerName参数,利用org.quartz.impl.StdSchedulerFactory的getScheduler方法创建sheduler实例,该实例实现了org.quartz.Scheduler接口;
- 根据传入的jobClass参数,决定创建何种Job实例,osworkflow自身提供了两种选择:WorkflowJob和LocalWorkflowJob。前者支持SOAP协议,后者则是本地调用,它们都实现了org.quartz.Job接口。
- 创建一个描述Job信息的JobDetail实例,并做好初始设置;
- 若传入参数中未指定cronExpression,则创建SimpleTrigger,并设置好startDate、endDate、repeat,否则创建CronTrigger
- 在jobDetail和trigger准备完毕后,就可以启动schedule了:

s.addJob(jobDetail, true);
s.scheduleJob(trigger);
s.start();

scheduler中应该可以同时维护多个job和trigger,当trigger的触发条件满足后,将会激活真正的job实例。由于,scheduler中只保存了jobDetail的实例,因此我猜想,job实例的真正创建是由jobDetail完成的。job实例(WorkflowJob、LocalWorkflowJob或者是其他自定义扩展类)激活后,其excute方法将会被执行。其内部的执行逻辑大体是获得指定的Workflow实例,然后执行该实例的executeTriggerFunction方法。trigger function的执行与先前在流程定义文件中所出现过的普通function大同小异。当然,我们还需要在流程定义文件中加入对trigger function的描述,大致格式如下:

<trigger-functions>
 <trigger-function id="1″ >
 <function>
  …
 </function>
</trigger-functions>

11/25/2004

osworkflow解读之四

Filed under: — site admin @ 9:38 am

流程启动>>

前面分析流程流转的执行逻辑时,并没有讲到流程的启动。实际上,有了前面的基础,再加上对流程定义文件加载的过程有清晰认识之后,流程启动逻辑是很容易理解的。大致情况如下:
- 调用DefaultConfiguration的getWorkflow,传入流程名称,然后返回一个WorkflowDescriptor实例,流程定义文件的加载就是在这个时候完成的;
- 然后做一些准备工作,比如:获取WorkflowStore实例、准备好transientVars和propertySet等等;
- 调用WorkflowDescriptor的getInitialAction方法,获取initial action(如果有的话),注意此前的第一步中,流程定义文件已经成功加载;
- 调用transitionWorkflow,执行流程的流转(就是前面提到doAction执行逻辑时调用的那个重要的方法);

在transitionWorkflow方法中,与流程启动后step间的流转稍有不同的是:当发现action为initial action时,将会把流程设置为activated状态,表示流程启动。就是下面这段代码:

if ((wf.getInitialAction(action.getId()) != null) && (entry.getState() != WorkflowEntry.ACTIVATED)) {
 changeEntryState(entry.getId(), WorkflowEntry.ACTIVATED);
}

WorkflowStore和WorkflowEntry>>

前面提到过,在AbstractWorkflow中,包含流程流转关键逻辑的transitionWorkflow方法,会创建新的step。而这一创建工作是通过调用另一个member method实现的,也就是createNewCurrentStep。其执行逻辑大致如下:

int nextStep = theResult.getStep();
if (nextStep == -1) {
 if (currentStep != null) {
  nextStep = currentStep.getStepId();
 }
}

if (currentStep != null) {
 store.markFinished(currentStep, actionId, new Date(), oldStatus, context.getCaller());
 store.moveToHistory(currentStep);
}

Step newStep = store.createCurrentStep(entry.getId(), nextStep, owner, startDate, dueDate, status, previousIds);

从这段代码中,我们首先可以看到,osworkflow可以支持step节点自身再次被“激活”的行为,也就是重复执行某个step。而另一方面,随后的创建工作则是调用了store.createCurrentStep。

这个store变量就是一个实现了WorkflowStore接口的实例变量,缺省是MemoryWorkflowStore。WorkflowStore除了创建step之外,还提供了一系列find和query方法。在其内部分别保存着step的历史记录(history steps)以及当前处于“激活”状态的step(current steps),以MemoryWorkflowStore为例,分别对应了两个HashMap,而JDBCWorkflowStore则利用数据库表来记录这些信息。

实际上,WorkflowStore可以同时保存多个流程的记录,这样就可以满足同时存在多个流程的情况。为此,osworkflow提供了一个WorkflowEntry接口用来描述流程信息,其中包含了流程名称、流程ID、当前状态等等。WorkflowEntry中定义了如下几个流程状态常量:

public static final int CREATED = 0; //创建
public static final int ACTIVATED = 1; // 激活
public static final int SUSPENDED = 2; // 挂起
public static final int KILLED = 3; // 异常终止
public static final int COMPLETED = 4; // 正常结束
public static final int UNKNOWN = -1;

在WorkflowStore提供的接口方法中有如下几个方法供维护WorkflowEntry使用:

public WorkflowEntry createEntry(String workflowName) throws StoreException;
public WorkflowEntry findEntry(long entryId) throws StoreException;
public void setEntryState(long entryId, int state) throws StoreException;

具体的方法实现,各个具现类有所不同。比如。在MemoryWorkflowStore中,是通过维护一个存储SimpleWorkflowEntry实例(实现了WorkflowEntry接口)的HashMap达到目的的。

11/24/2004

OSWorkflow解读之三

Filed under: — site admin @ 9:48 am

初始配置加载及工作流定义文件加载>>

AbstractWorkflow首先会取得一个Configuration实例,缺省时为DefaultConfiguration(实现了Configuration接口),该实例负责系统配置的加载。AbstractWorkflow会调用其load方法,该方法内部会查找一个名为osworkflow.xml的配置文件,并对其解析。osworkflow.xml文件中一般会指定persistence class和factory class,比如:

<osworkflow>
 <persistence class="com.opensymphony.workflow.spi.memory.MemoryWorkflowStore"/>
 <factory class="com.opensymphony.workflow.loader.XMLWorkflowFactory">
  <property key="resource” value="workflows.xml” />
 </factory>
</osworkflow>

load方法会动态加载这些class,还可以指定一些参数,在它们初始化的时候传入。在这里,我们指定了XMLWorkflowFactory,实际上还有其他种类的WorkflowFactory,比如JDBCWorkflowFactory、URLWorkflowFactory,它们均派生自AbstractWorkflowFactory,其共同职责是通过某种媒介加载流程定义。

以XMLWorkflowFactory为例,其相应实例在load方法内部完成初始化的过程中,将会查找一个名为workflows.xml的文件,可以在该文件中定义多个流程,每个流程指明其对应的xml定义文件。比如:

<workflows>
 <workflow name="example” type="resource” location="example.xml"/>
</workflows>

在XMLWorkflowFactory内部维护了一个Map,保存着流程名称与其对应的文件路径(实际上是一个WorkflowConfig实例,不过这只是细节)。然后,DefaultConfiguration调用XMLWorkflowFactory.getWorkflow方法,并传入流程名称。

getWorkflow方法内部又会将流程加载的具体执行逻辑转交给一个名为WorkflowLoader的类(调用WorkflowLoader.load方法),由WorkflowLoader实现流程定义文件读取。不过,真正的xml文件解析是由WorkflowDescriptor类完成的。它将平面的xml流转化为osworkflow内部所使用的具有真正意义的对象。此类有很多get方法,在osworkflow的许多地方都将会用到这个类的get方法,以获取具体的对象,比如:getAction,getJoin,getStep等等。

最终,代表example.xml流程定义文件的WorkflowDescriptor实例将会被逐层返回,直至AbstractWorkflow。至此,流程定义文件加载完毕,整个初始化过程也就基本完成了。

11/23/2004

OSWorkflow解读之二

Filed under: — site admin @ 9:09 am

多种方式定制逻辑>>

osworkflow的几个基本元件都具有很好的扩展性,它们分别是:condition,function,register,validator。以condition为例,我们可以为触发action的condition定义任意复杂的逻辑,而这种逻辑可以包含在一个java class中,也可以采用bsf,或者bean shell,还有local ejb和remote ejb。只要流程定义文件做相应配置即可,以java class和bean shell为例:

<action id="1″ name="Start Workflow">
 <restrict-to>
  <conditions type="AND">
   <condition type="beanshell">
    <arg name="script">true</arg>
   </condition>
   <condition type="class">
    <arg name="class.name">com.opensymphony.workflow.util.OSUserGroupCondition</arg>
    <arg name="group">foos</arg>
   </condition>
  </conditions>
 </restrict-to>
 …
</action>

在osworkflow的许多地方都可以见到类似如下的代码(此处以function为例):

if ("remote-ejb".equals(type)) {
  clazz = RemoteEJBFunctionProvider.class.getName();
} else if ("local-ejb".equals(type)) {
  clazz = LocalEJBFunctionProvider.class.getName();
} else if ("jndi".equals(type)) {
  clazz = JNDIFunctionProvider.class.getName();
} else if ("bsf".equals(type)) {
  clazz = BSFFunctionProvider.class.getName();
} else if ("beanshell".equals(type)) {
  clazz = BeanShellFunctionProvider.class.getName();
} else {
  clazz = (String) args.get(CLASS_NAME);
}
FunctionProvider provider = (FunctionProvider) loadObject(clazz);
provider.execute(transientVars, args, ps);

loadObject会动态加载相应的具现处理类(比如BSFFunctionProvider),并转换为基类类型(比如FunctionProvider),然后调用相应的执行逻辑(比如provider.execute)。这一模式屡试不爽。

11/22/2004

OSWorkflow解读之一

Filed under: — site admin @ 9:35 am

AbstractWorkflow>>

osworkflow中有关工作流流转的所有核心代码都在AbstractWorkflow中,BasicWorkflow就是派生自它,不过这个BasicWorkflow基本上没做什么事情。也许我们还可以从AbstractWorkflow派生自己的Workflow类以加入扩展功能,大概这也算是osworkflow所体现的一种灵活性了,即:允许对工作流流转的执行逻辑进行修改。AbstractWorkflow实现了Workflow接口,该接口包含了有关工作流的核心方法,最重要的是doAction方法,AbstractWorkflow实现了该方法,后面会提及,其他还有一些getter和query method。

流程流转的执行逻辑>>

当流程执行到的某个step时,可能有一个或多个action可供用户选择执行。一旦确定执行某个action后,我们需要调用AbstractWorkflow.doAction,并传入流程id和action的id。以下是对doAction的执行逻辑的一个不太严紧的算法描述:

*  *  *

-  根据流程id,获得所有当前的step,这种情况往往发生在有split的时候,此时会有多个step等待执行;
-  根据传入的action的id,检查是否是global action;
-  若不是global action,则遍历所有当前的step,对每个step的每个action调用isActionAvailable方法,检查该action是否可用(记住step和action是一对多的关系);
-  所谓可用是指,通过执行passesConditions,逐个检查action的condition:若是OR的关系,则有一个condition为真即为可用,AND关系则类推;
-  若action可用,则调用transitionWorkflow,这是流程流转处理的关键部分;
 执行transitionWorkflow时:
 -  首先获取当前step,存在有多个当前step的情况,比如split,此时获取首个isAvailableAction为真的step;
 -  调用verifyInputs验证输入(如果action有validator的话);
 -  执行当前step的post function(因为该step即将结束);
 -  执行action的pre function;
 -  判断当前step所属的result中的所有condition是否满足要求,判断方法类似action的condition;
 -  一旦满足,则获取result的pre function和post function;
 -  否则即是unconditional result,获取相应的pre function和post function;
 -  在没有split和join的情况下
  -  会根据在result中指定的下一个step的id,创建一个新的step,作为当前的step;
  -  从current steps中移除原来的当前step,并添加到history steps中;
  -  如果新的step有pre function,则会马上执行;
 -  执行result的post function;
 -  执行action的post function;
 -  若action是intial action,则将流程设置为activated状态;
 -  若action是finish action,则将流程设置为completed状态,返回true;
 -  寻找auto action,若有的话,则执行之,执行方法是调用doAction本身;
 -  返回false;
-  根据transitionWorkflow的返回值判断流程是否结束;
-  若返回false,则调用checkImplicitFinish检查是否存在implicit finish,即:当前没有一个step的action可用时,就认为流程应该结束;

*  *  *

- 若存在split,则会创建多个新的step,并且在创建之前先执行split的pre function,在创建之后执行split的post function;
- 创建step的过程和上面描述的普通状况相同:维护好current steps和history steps,并执行新的step的pre function;

*  *  *

- 若存在join,先结束当前step,并将该step添加至history steps和join steps;
- 查找history steps,对每个已完成的step,查看是否在其result或unconditional result中有join一项,若有则加入join steps中;
- 检查join是否已经满足:可以使用Bean Shell,在xml定义文件的join节点中,通过引用一个名为“jn”的特殊变量来指定join的满足条件,jn记录了有关join的关键信息;
- 若条件满足,则执行join的pre function,维护好history steps,并创建下一个step,然后执行join的post function;

*  *  *

- 对于条件循环的情况,可以通过将result的某个action的下一个step指定为自身来加以实现,这只是在xml定义文件中做文章,流程执行逻辑无需做特殊处理;

11/20/2004

time granulrity,issue tracking,tools vs. human

Filed under: — site admin @ 10:12 pm

time granulrity:

作为时间的一种度量单位,或小时记,或天记,都是灵活的和因人而异的。比如你完全可以将8个小时理解为1天。但是,粒度的确定并非没有原则:为任务分配时间是一种预估,这样的估计如果易变而不准确,那么多半也就失去了意义。一般而言,只有在对所做的工作十分了解的前提下,才能够准确估计。对于一个易变的的环境,陌生的环境,时常被打断的环境,或者时常冒出些“障碍”的环境(比如某个艰深的bug fix),时间的粒度就不亦过细,至少在迭代的初期是如此。随着时间的推移和经验的积累,估计的粒度也就会相应改进。

issue tracking

issue tracking至关重要,否则事态就可能滑入不可控的危险,team member就会失去对项目状况的全景认识,这也是我的切身体会。实际上,不单是bug,feature,defact,……,很多东西都是需要有tracking的。比如某此会议中的一个idea,如果没有某种形式的tracking,很容易就会消失在大家的记忆中,直到哪次再被大家带着一种似曾相识的感情从被遗忘的角落里重新提起。

tools vs. human

我始终觉得,在一个开发团队中,对于工具的使用,总是二等考量,更为重要的是人的因素。因为,即使有趁手的工具,也依然有可能存在缺乏交流的team member,以及由这些team member参加的冗长的缺乏主题的会议。

11/17/2004

OSWorkflow开篇

Filed under: — site admin @ 11:04 pm

刚刚下载了OSWorkflow,这里摘录的是联机文档中的开篇部分,也就是OSWorkflow的核心概念了:

OSWorkflow is based heavily on the concept of the finite state machine. Each state is represented by the combination of a step ID and a status. A transition from one state to another cannot happen without an action occuring first. There are always at least one or more active states during the lifetime

11/16/2004

计划的粒度

Filed under: — site admin @ 10:17 pm

一个计划的制订到底是以什么为单位才更合理呢?记得同事以前曾因计划粒度过细而有所抱怨。不习惯计划的人,会为难于周计划;习惯了周计划的人,会为难于日计划。好在我以前就已经有了写日志的习惯,因此对周计划并没有什么不习惯的地方。

不过今天到是翻然醒悟,绕了一个大圈子之后才发现,至少以我目前的能力而言,要将计划的粒度控制到小时为单位还是有些困难。其一是精力的不自主分散;其二是不确定因素的干扰。前者还需日后磨炼,而后者却是不可控的。

不过以天为单位的计划任务还是可取的,“多还少补”,保证大方向不变。持之以恒的小幅前进,也是一种成就感。

测试先行的合理性

Filed under: — site admin @ 10:22 am

如果你对自己把握OOD的能力还没有足够的信心,如果你对需求的理解还不甚明晰而无从下手;如果你更习惯于从代码中寻找问题的答案(这是许多程序员一贯的品质),那么就先从编写testcase开始吧,它可以有助于你抛开一些既有设计中华而不实的先验假设和过分设计,从而让你变得注重实际。一组testcase下来,OOD也就完成了一半了。

11/15/2004

To Maven or to Ant ?

Filed under: — site admin @ 9:52 pm

maven最具特色的两点是POM和repository,前者是描述项目对象的基础概念,对应于project.xml文件,而后者则很好的解决了项目依赖问题,包括对第三方库的依赖。相比而言,我觉得后一点更为重要一些。利用集中式的构建资源库,只需要指定jar包的名称和版本,maven就会自动下载对应的snapshot。不过,在我看来这一点用ant也可以做到,只是在maven这边属于内建功能,平滑而自然。而ant则需要更多的人工参与才能达到,不过这对于一个已经经过精心规划的ant-based项目而言,应该算不上什么,积累很重要。

maven的出现晚于ant,实际上其诸多思想都是借鉴ant而来的,并做了改进。它将不少ant中的潜规则“固化”于软件本身,比如目录结构的安排,又比如build.properties文件的加载。这些features为初涉maven者提供了便利。

另外,maven的jelly脚本带有procedure的味道,这与ant是有所不同的(当然ant中也可以内嵌脚本)。尽管适当的过程控制语义可以另问题妥善而方便的解决,不过请记住:无论maven也好,ant也好,毕竟都不是一门过程语言,不要赋予它们过多的责任。

一个好的工作流引擎

Filed under: — site admin @ 5:45 pm

从对流程表达的角度来看,我认为一个好的工作流引擎,首先从表达能力上应该“极力”做到丰富,而在表达形式上则应该“极力”做到简单。如果能够在具备很好的表达各种复杂工作流模式的同时,又不致让使用者陷入模式概念的泥淖中,亦即,用简洁的表现力来涵盖复杂的流程模式,那就成功了一半了。当然,要做到这一点也许并不容易。

11/14/2004

对Maven大致了解之后的一点整理结果

Filed under: — site admin @ 5:55 pm

Jelly>>
Maven利用Jelly标签库来进行脚本的编写工作。Jelly变量的定义规则与Ant的proeprty不同,赋值后仍可以修改。Jelly脚本是带有procedure痕迹的,比如<foreach>。

jeez标签库>>
jeez标签库已由Maven预先注册,jeez同时包含了ant和werkz标签。因而可以在脚本中直接使用合法的ant task,而无需指定前缀,从而简化了ant的迁移过程。

goal>>
goal有些类似ant的target,并且它是一个werkz标签。一个goal可以包含任何合法的ant标签。在执行指定插件的goal时,需要在goal前面加上插件名作为前缀。

插件>>
Mave利用插件来为紧凑的内核添加丰富的功能。Maven自带了许多插件包。

POM>>
Maven基于POM概念,POM对应于一个project.xml,利用POM的继承机制,我们可以将一些公用的定义内容单独放到一个project.xml中作为父POM,而在各自的子POM中,只编写各异的部分。

maven.xml>>
如果需要对Maven的执行过程做定制,则需要编写maven.xml文件。maven.xml依然使用Jelly脚本。maven.xml由一个<project>开始,内含若干<goal>,每个<goal>则引入脚本的执行逻辑。procedure smell就是在这里得以体现的。

Maven的属性处理机制>>
Maven依次调用下满的属性文件:
${project.home}/project.properties
${project.home}/build.properties
${user.home}/build.properties
Maven的属性覆盖规则正好与Ant相反,“最后定义有效”,由于-D依然具有最高的优先级,因此其处理顺序被排在了最后。

而插件的属性规则又刚好相反,Maven禁止插件的属性覆盖已经设置的属性。由于Maven是在处理完所有其他属性之后再开始处理插件的,因此,我们预先设置的某些属性,就可以反过来对插件的某些缺省属性做“覆盖”,这个规则很像Ant的“property immutable”规则。

Maven提供了缺省的一系列系统属性,对它们的修改,可以定制Maven的行为。

Repository机制>>
Maven采用集中管理库的方式。所有要引用的jar,都统一放到一个repository中,本地或者远程,并且可以实现自动下载,从而解决了项目间共享第三方依赖包的问题。这一点很值得借鉴。主repository位于Ibiblio站点,但是你也可以自己创建一 个远程repositories。

SNAPSHOT>>
建立SNAPSHOT依赖后,对Jar包的依赖可以实现即时的同步。

11/13/2004

对Ant又有了新的发现

Filed under: — site admin @ 5:57 pm

看来应该早一点看看有关Ant 1.6的新特性,关于MacroDef,Import,subant,虽然我现在对他们的使用还不太熟悉,但是我觉得这些正是可以解决我这一周来在使用Ant解决多工程构建与依赖时所遇到的困难。

MacroDef是一种简单的自定义Task的方法(一组现成Task的序列,有别于自己手工编写Task),其调用行为类似<antcall>,不过它还可以和<import>结合,并且显示输出上更友好。本质上感觉<antcall>更带有procedure的味道,而MacroDef则更为自然,也比较符合Ant的精神。

import的功能类似于原来的xml entity import,但是它在target overriding和special properties两个方面对原始的xml entity import做了很大的改进。看来《Java Dev. with Ant》确实有一点点过时了。

subant是批量执行build.xml的指定target的简便方法。

有了这些工具,我的build脚本编写也就不用那么辛苦了。看来过段时间等我有了更进一步的掌握之后,我得重构我的那帮build.xml哥们了。

11/9/2004

JCoverage使用中的几个问题

Filed under: — site admin @ 11:51 am

昨天和今天被JCoverage搞的头都大了,一番折腾下来,小结如下:
- 奇怪的库依赖,instrument的时候总是报ClassDefNoFound,说找不到log4j。开始百思不得其解,后来才发现在taskdef的时候应该在classpathref中指定log4j的所在路径,而不只是jcoverage的jar包路径。
- CreatePross时的IOException,一旦instrument的类超过某个数量(比如800个),就会出现这样的异常,堆栈显示错误的地方在org.apache.tools.ant.taskdefs.Java.fork。这恐怕和jcoverage自身有关,目前最简单的办法就是不要一次“注入”这么多类,可以分批依次“注入”。不然就得看instrument的源代码了。
- instrument无法识别匿名类。尽管以debug方式编译源码,可依然有WARN蹦出,说无法找到***$1的souce line number。
- 没有详细的帮助文档,恐怕开源的东西也真的不好苛求,不知道JCoverage的商业版本是否有所改善,包括前面那个IOException。
- 没有源代码报告的交叉引用,功能上讲这是一个比较大的欠缺,据说Clover就有此项功能。

11/7/2004

不要迷信UML的权威性

Filed under: — site admin @ 11:00 am

昨天看完了Robert Martin那本Agile Software Development的第六章。这一章讲述的是大师们的peer-programming实践,以一种非常写实的笔法对过程做了全程记录:设计、编写测试、实现代码、修改测试、重构代码……这些环节是经过多次迭代才得以完成的。设计中隐藏着错误,错误的讯号会反复出现,测试用例和代码是在思路的不段前进中逐步明晰与确定的。这是我第一次看到如此鲜活生动的例子,非常精彩的篇章。

对本章的结论我也颇有同感,并非到处都需要OOD,尤其对于简单的应用(又是over desgin的味道)。

更为重要的一点是,对UML的清醒认识。这段peer-programming经历中并没有完整的设计过程,从一点点简单的设计出发,首先开始的是测试用例的编写。

事情的结果告诉我们,UML图并不能避免我们在设计中犯错误,也不一定能帮助我们发现错误,即使有一套完善的序列图,错误也依然存在。也许更为糟糕的是,漂亮的UML图容易让我们盲目的坚定原来那些错误的观念和设计。同时,UML图也分散了我们的精力,极有可能导致设计的复杂性,滑向over design的危险边缘。

人非圣贤,就是大师级的R. C. Martine也不例外,唯有编码实践才最容易找到错误所在。通过画图来展开思路本没有错,但是在画完UML图示而又没有验证代码可循时,图示就是无益的,就是危险的。不能假定这种未经证实的设计是最好的。最好的设计是在编写测试,然后小幅前进的过程中逐渐形成的。这也就是测试先行的必要性所在。

11/5/2004

持续集成平台实现产品完整自动构建

Filed under: — site admin @ 10:59 am

这两天对AnthillPro持续集成平台在产品代码自动构建方面做了一些试验。

现在AnthillPro每天会自动构建一个产品的版本,源代码位于cvs中,同时还会生成相关的单元测试报告、压力测试报告,以及测试覆盖报告。每天,team member可以通过内部web直接查看这些报告。另外,成熟代码的对外打包发布也实现了自动完成。只要通过AnthillPro做一次No Schedule的Release Build,相应的zip包就会放到用户可以通过网页直接访问并下载的地方。

目前还有一点不足是,没有成功实现自动热部署。一旦实现构建过程中的热部署,那么对于web应用而言,自动构建成功之后,就意味着可以直接通过web方式访问站点。这对处于开发阶段的team member而言可能是很有价值的。我在本地的Windows环境已经调通,但是到了linux下就不灵了。我用的是tomcat提供的Manage API接口,也许是tomcat的热部署还不成熟,总之有待查证。

另外,作为补充,我还打算将CheckStyle加入构建过程,生成的报表同样可以直接访问到。接下来还有一项工作就是,试验AnthillPro环境下,针对大型复杂项目/产品的master build + child build工程构建方式。

基于Ant的集成开发方式——多层面的properties(续)

Filed under: — site admin @ 10:20 am

改动频繁的properties应该独立于build.xml,单独作为外部properties文件。尤其是用户相关的properties,也就是所谓的user-specific properties。这样的文件不用放到SCM中,而是放到各自本地机的user.home中。

长期不变的properties通常可以放到build.xml文件内的首部。这些properties也有存在的必要,开发阶段的偶尔调整是有可能的,它们的作用就像程序设计中的常量定义——改动一处,全盘皆变——方便而又易于维护。当然,这些properties应该是随build.xml文件一起保存在SCM中的。

更复杂的手段是:

除user-specific配置项外,针对项目的一些全局性配置提供project-specific的properties文件。这些配置项的易变性往往低于user-specific配置项,但是长期的开发周期中依然有可能变动,此类properties文件应该放入SCM中。一个最明显的例子就是project.version,该配置项的用途之一是生成可发布软件时作为路径或文件名的一部分,就像tomcat的home路径通常是jakarta-tomcat-

user-specific配置项和project-specific配置项允许互有交叉。当两者冲突时,user-specific可以覆盖project-specific,即前者的优先级更高。根据“immutable properties”原则,这就意味着,build.xml文件中应该首先加载user-specific properties。

根据实际情况,可能还有一类最易变的配置项。那就是每次build过程中都可能变化的properties,称之为build-specific properties。此时,我们可以利用Ant的命令行参数-D来进行配置。由Ant的特性我们可知,此类配置项的优先级是最高的。当然,一般情况下这种配置项还是少一些为好。

加上前面提到的位于build.xml文件内的properties,我将其暂命名为inner properties,各类proeprties的易变性和优先级情况如下:

类别 易变性 优先级 是否位于SCM中
build-specific properties No
user-specific properties No
project-specific properties Yes
inner properties Yes

综上,四种不同级别的properties,足够我们应付大多数项目构建了。

11/4/2004

基于Ant的集成开发方式——多层面的properties

Filed under: — site admin @ 11:12 am

开发阶段,不同用户环境下对程序的发布、部署要求可能不同,比如:配置参数的设置,系统的安装路径,第三方库的路径(假如不打算将第三方库加入发布的ZIP包内的话)。我们可以在编写build文件时借助定制properties来解决这个问题,即将一些配置项抽象为properties,也可以将这些properties单独放在一个外部文件里。在多人协作开发时,如果将这些易变的properties也加入CVS的话,则每此不同人员从CVS下载程序到本地时就必须再次更改,而这种更改可能要伴随CVS中相应文件的更新(build文件,或者是独立的properties文件),这显然是不合适的。

为此,我们可以利用每台本地机上的user.home路径,将properties文件放到那里。该文件不用放到CVS中,build时让Ant自动去目录下找该文件并加载。至于user.home路径,linux下为/home/,windows下一般为C:Document and Settings。其中为当前用户名。也可以在Ant中利用系统环境变量结合进行设置,这样更为灵活。windwos下的环境变量为HOMEPATH,linux下为HOME。

前面提到的properties文件单纯用于build过程,对于最终发布系统中所带的properties文件,我们也可以做类似的灵活性改进。首先编写一个properties的tmplate文件放入CVS中,在build文件的init target中先将tmplate文件copy成“实例”文件,然后将某些可配置项做字符串替换。我们可以将这些可配置项的替换值放到另一个properties文件中,而该文件则不放入CVS,也同样的放到user.home路径下。字符串替换可以用或者

当然,在发布完整的安装包时,可能也需要将这些散落在本地机的properties文件“加入”到安装包中。一种办法是,编写一个示范性的template文件,加入到CVS中,开发阶段并不使用该文件。在生成安装包时将该template文件也打入包中,同时,要在随产品发布的安装文档中加入相关说明。这样,用户就可以按照说明,手工copy一个“实例”文件,然后做一些针对性的配置。这种方法的不足在于要求用户做额外的工作,如果安装包自包含特性很好的话,那么用户也许只需要简单的运行一下run.bat(对于可运行版本而言),或者build.bat(即使是可运行版本也可能存在build过程,就像AnthillPro)。为此,我们需要在完成发布任务的Target中生成一个“final release”的properties文件,这样的文件只用于最终发布是打入包中。

11/2/2004

基于Ant的集成开发方式

Filed under: — site admin @ 10:26 am

开发阶段,不同用户环境下对程序的发布、部署要求可能不同,比如:配置参数的设置,系统的安装路径,第三方库的路径(假如不打算将第三方库加入发布的ZIP包内的话)。我们可以在编写build文件时借助定制properties来解决这个问题,即将一些配置项抽象为properties,也可以将这些properties单独放在一个外部文件里。在多人协作开发时,如果将这些易变的properties也加入CVS的话,则每此不同人员从CVS下载程序到本地时就必须再次更改,而这种更改可能要伴随CVS中相应文件的更新(build文件,或者是独立的properties文件),这显然是不合适的。

为此,我们可以利用每台本地机上的user.home路径,将properties文件放到那里。该文件不用放到CVS中,build时让Ant自动去目录下找该文件并加载。至于user.home路径,linux下为/home/,windows下一般为C:Document and Settings。其中为当前用户名。也可以在Ant中利用系统环境变量结合进行设置,这样更为灵活。windwos下的环境变量为HOMEPATH,linux下为HOME。

前面提到的properties文件单纯用于build过程,对于最终发布系统中所带的properties文件,我们也可以做类似的灵活性改进。首先编写一个properties的tmplate文件放入CVS中,在build文件的init target中先将tmplate文件copy成“实例”文件,然后将某些可配置项做字符串替换。我们可以将这些可配置项的替换值放到另一个properties文件中,而该文件则不放入CVS,也同样的放到user.home路径下。字符串替换可以用或者

当然,在发布完整的安装包时,可能也需要将这些散落在本地机的properties文件“加入”到安装包中。一种办法是,编写一个示范性的template文件,加入到CVS中,开发阶段并不使用该文件。在生成安装包时将该template文件也打入包中,同时,要在随产品发布的安装文档中加入相关说明。这样,用户就可以按照说明,手工copy一个“实例”文件,然后做一些针对性的配置。这种方法的不足在于要求用户做额外的工作,如果安装包自包含特性很好的话,那么用户也许只需要简单的运行一下run.bat(对于可运行版本而言),或者build.bat(即使是可运行版本也可能存在build过程,就像AnthillPro)。为此,我们需要在完成发布任务的Target中生成一个“final release”的properties文件,这样的文件只用于最终发布是打入包中。

11/1/2004

关于Code Coverage

Filed under: — site admin @ 4:56 pm

在整理有关Code Coverage的资料时,从一篇blog上看到的内容,现依据作者观点,整理思路如下:

代码覆盖率用于衡量测试程度充分与否,从而给开发者以信心:

- 代码覆盖率有助于度量测试质量。软件度量技术的不成熟,导致不存在放之四海而皆准的度量方案:考察产品发布后客户反馈的bug数量,很难界定测试和开发的责任,同时也已于事无补;而对于需求/功能覆盖率测试而言,实际的需求文档通常是不完整的,并且人员对需求理解也常常存在偏差。代码覆盖率这一重要指标,应该得到重视的。

- 代码覆盖率有助于风险分析。No risk, no test. 风险分析的准确性主要依赖于分析者的技术素养和其对系统的了解程度。代码覆盖率有助于分析者了解系统,在进行非正式风险分析时,不应该拒绝任何可能有用的信息。

- 开发人员更愿意阅读代码覆盖报告这样看起来比较专业的测试成果。

但是,100%的覆盖是不可能的;并且,即便能做到100%覆盖,也不能证明软件被充分测试,因为每种覆盖算法(路径、语句、方法)都有不足。

那么,覆盖率达到多少为宜?——将代码覆盖报告看作测试的线索即可,并不过分依赖于它。

Powered by WordPress