osworkflow 入门基础
OSWorkFlow入门指南
目的
这篇指导资料的目的是介绍OSWorkflow的所有概念,指导你如何使用它,并且保证你逐步理解OSWorkflow的关键内容。
本指导资料假定你已经部署OSWorkflow的范例应用在你的container上。范例应用部署是使用基于内存的数据存储,这样你不需要担心如何配置其他持久化的例子。范例应用的目的是为了说明如何应用OSWorkflow,一旦你精通了OSWorkflow的流程定义描述符概念和要素,应该能通过阅读这些流程定义文件而了解实际的流程。
本指导资料目前有3部分:
1. 你的第一个工作流
2. 测试你的工作流
3. 更多的流程定义描述符概念
1. Your first workflow
创建描述符
首先,让我们来定义工作流。你可以使用任何名字来命名工作流。一个工作流对应一个XML格式的定义文件。让我们来开始新建一个“myworkflow.xml”的文件,这是样板文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE workflow PUBLIC
"-//OpenSymphony Group//DTD OSWorkflow 2.7//EN"
"http://www.opensymphony.com/osworkflow/workflow_2_7.dtd">
<workflow>
<initial-actions>
...
</initial-actions>
<steps>
...
</steps>
</workflow>首先是标准的XML头部,要注意的是OSWorkflow将会通过这些指定的DTD来验证XML内容的合法性。你可以使用绝大多数的XML编辑工具来编辑它,并且可以highlight相应的错误。
步骤和动作
接下来我们来定义初始化动作和步骤。首先需要理解的OSWorkflow重要概念是steps (步骤) 和 actions (动作)。一个步骤是工作流所处的位置,比如一个简单的工作流过程,它可能从一个步骤流转到另外一个步骤(或者有时候还是停留在一样的步骤)。举例来说,一个文档管理系统的流程,它的步骤名称可能有“First Draft - 草案初稿”,“Edit Stage -编辑阶段”,“At publisher - 出版商”等。
动作指定了可能发生在步骤内的转变,通常会导致步骤的变更。在我们的文件管理系统中,在“草案初稿”这个步骤可能有“start first draft - 开始草案初稿”和“complete first draft - 完成草案初稿”这样2个动作。
简单的说,步骤是“在哪里”,动作是“可以去哪里”。
初始化步骤是一种特殊类型的步骤,它用来启动工作流。在一个工作流程开始前,它是没有状态,不处在任何一个步骤,用户必须采取某些动作才能开始这个流程。这些特殊步骤被定义在 <initial-actions>。
在我们的例子里面,假定只有一个简单的初始化步骤:“Start Workflow”,它的定义在里面<initial-actions>:
<action id="1" name="Start Workflow">
<results>
<unconditional-result old-status="Finished" status="Queued" step="1"/>
</results>
</action>这个动作是最简单的类型,只是简单地指明了下一个我们要去的步骤和状态。
工作流状态
工作流状态是一个用来描述工作流程中具体步骤状态的字符串。在我们的文档管理系统中,在“草案初稿”这个步骤可能有2个不同的状态:“Underway - 进行中”和“Queued - 等候处理中”
我们使用“Queued”指明这个条目已经被排入“First Draft”步骤的队列。比如说某人请求编写某篇文档,但是还没有指定作者,那么这个文档在“First Draft”步骤的状态就是“Queued”。“Underway”状态被用来指明一个作者已经挑选了一篇文档开始撰写,而且可能正在锁定这篇文档。
第一个步骤
让我们来看第一个步骤是怎样被定义在<steps>元素中的。我们有2个动作:第一个动作是保持当前步骤不变,只是改变了状态到“Underway”,第二个动作是移动到工作流的下一步骤。我们来添加如下的内容到<steps>元素:
<step id="1" name="First Draft">
<actions>
<action id="1" name="Start First Draft">
<results>
<unconditional-result old-status="Finished"
status="Underway" step="1"/>
</results>
</action>
<action id="2" name="Finish First Draft">
<results>
<unconditional-result old-status="Finished"
status="Queued" step="2"/>
</results>
</action>
</actions>
</step>
<step id="2" name="finished" />这样我们就定义了2个动作,old-status属性是用来指明当前步骤完成以后的状态是什么,在大多数的应用中,通常用"Finished"表示。
上面定义的这2个动作是没有任何限制的。比如,一个用户可以调用action 2而不用先调用action 1。很明显的,我们如果没有开始撰写草稿,是不可能去完成一个草稿的。同样的,上面的定义也允许你开始撰写草稿多次,这也是毫无意义的。我们也没有做任何的处理去限制其他用户完成别人的草稿。这些都应该需要想办法避免。
让我们来一次解决这些问题。首先,我们需要指定只有工作流的状态为“Queued”的时候,一个caller (调用者)才能开始撰写草稿的动作。这样就可以阻止其他用户多次调用开始撰写草稿的动作。我们需要指定动作的约束,约束是由Condition(条件)组成。
条件
OSWorkflow 有很多有用的内置条件可以使用。在此相关的条件是“StatusCondition - 状态条件”。 条件也可以接受参数,参数的名称通常被定义在javadocs里(如果是使用Java Class实现的条件的话)。在这个例子里面,状态条件接受一个名为“status”的参数,指明了需要检查的状态条件。我们可以从下面的xml定义里面清楚的理解:
<action id="1" name="Start First Draft">
<restrict-to>
<conditions>
<condition type="class">
<arg name="class.name">
com.opensymphony.workflow.util.StatusCondition</arg>
<arg name="status">Queued</arg>
</condition>
</conditions>
</restrict-to>
<results>
<unconditional-result old-status="Finished" status="Underway" step="1"/>
</results>
</action>希望对于条件的理解现在已经清楚了。上面的条件定义保证了动作1只能在当前状态为“Queued”的时候才能被调用,也就是说在初始化动作被调用以后。
函数
接下来,我们想在一个用户开始撰写草稿以后,设置他为“owner”。为了达到这样的目的,我们需要做2件事情:
1) 通过一个函数设置“caller”变量在当前的环境设置里。
2) 根据“caller”变量来设置“owner”属性。
函数是OSWorkflow的一个功能强大的特性。函数基本上是一个在工作流程中的工作单位,他不会影响到流程本身。举例来说,你可能有一个“SendEmail”的函数,用来在某些特定的流程流转发生时来发送email提醒。
函数也可以用来添加变量到当前的环境设置里。变量是一个指定名称的对象,可以用来在工作流中被以后的函数或者脚本使用。
OSWorkflow提供了一些内置的常用函数。其中一个称为“Caller”,这个函数会获得当前调用工作流的用户,并把它放入一个名为“caller”的字符型变量中。
因为我们需要追踪是哪个用户开始了编写草稿,所以可以使用这个函数来修改我们的动作定义:
<action id="1" name="Start First Draft">
<pre-functions>
<function type="class">
<arg name="class.name">
com.opensymphony.workflow.util.Caller</arg>
</function>
</pre-functions>
<results>
<unconditional-result old-status="Finished"status="Underway"
step="1" owner="${caller}"/>
</results>
</action>h3 组合起来
把这些概念都组合起来,这样我们就有了动作1:
<action id="1" name="Start First Draft">
<restrict-to>
<conditions>
<condition type="class">
<arg name="class.name">
com.opensymphony.workflow.util.StatusCondition
</arg>
<arg name="status">Queued</arg>
</condition>
</conditions>
</restrict-to>
<pre-functions>
<function type="class">
<arg name="class.name">
com.opensymphony.workflow.util.Caller
</arg>
</function>
</pre-functions>
<results>
<unconditional-result old-status="Finished"status="Underway"
step="1" owner="${caller}"/>
</results>
</action>我们使用类似想法来设置动作2:
<action id="2" name="Finish First Draft">
<restrict-to>
<conditions type="AND">
<condition type="class">
<arg name="class.name">
com.opensymphony.workflow.util.StatusCondition
</arg>
<arg name="status">Underway</arg>
</condition>
<condition type="class">
<arg name="class.name">
com.opensymphony.workflow.util.AllowOwnerOnlyCondition
</arg>
</condition>
</conditions>
</restrict-to>
<results>
<unconditional-result old-status="Finished" status="Queued" step="2"/>
</results>
</action>在这里我们指定了一个新的条件:“allow owner only”。这样能够保证只有开始撰写这份草稿的用户才能完成它。而状态条件确保了只有在“Underway”状态下的流程才能调用“finish first draft”动作。
把他们组合在一起,我们就有了第一个流程定义:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE workflow PUBLIC
"-//OpenSymphony Group//DTD OSWorkflow 2.7//EN"
"http://www.opensymphony.com/osworkflow/workflow_2_7.dtd">
<workflow>
<initial-actions>
<action id="1" name="Start Workflow">
<results>
<unconditional-result old-status="Finished"
status="Queued" step="1"/>
</results>
</action>
</initial-actions>
<steps>
<step id="1" name="First Draft">
<actions>
<action id="1" name="Start First Draft">
<restrict-to>
<conditions>
<condition type="class">
<arg name="class.name">
com.opensymphony.workflow.util.StatusCondition
</arg>
<arg name="status">Queued</arg>
</condition>
</conditions>
</restrict-to>
<pre-functions>
<function type="class">
<arg name="class.name">
com.opensymphony.workflow.util.Caller
</arg>
</function>
</pre-functions>
<results>
<unconditional-result old-status="Finished" status="Underway"
step="1" owner="${caller}"/>
</results>
</action>
<action id="2" name="Finish First Draft">
<restrict-to>
<conditions type="AND">
<condition type="class">
<arg name="class.name">
com.opensymphony.workflow.util.StatusCondition
</arg>
<arg name="status">Underway</arg>
</condition>
<condition type="class">
<arg name="class.name">
com.opensymphony.workflow.util.
AllowOwnerOnlyCondition
</arg>
</condition>
</conditions>
</restrict-to>
<results>
<unconditional-result old-status="Finished"
status="Queued" step="2"/>
</results>
</action>
</actions>
</step>
<step id="2" name="finished" />
</steps>
</workflow>现在这个工作流的定义已经完整了,让我们来测试和检查它的运行。
2. Testing your workflow
现在我们已经完成了一个完整的工作流定义,下一步是检验它是否按照我们预想的方式执行。
在一个快速开发环境中,最简单的方法就是写一个测试案例。通过测试案例调用工作流,根据校验结果和捕捉可能发生的错误,来保证流程定义的正确性。
我们假设你已经熟悉Junit和了解怎样编写测试案例。如果你对这些知识还不了解的话,可以去JUnit的网站查找、阅读相关文档。编写测试案例会成为你的一个非常有用的工具。
在开始载入流程定义、调用动作以前,我们需要配置OSWorkflow的数据存储方式和定义文件的位置等。
配置 osworkflow.xml
我们需要创建的第一个文件是 osworkflow.xml。子:
<osworkflow>
<persistence class="com.opensymphony.workflow.
spi.memory.MemoryWorkflowStore"/>
<factory class="com.opensymphony.workflow.loader.XMLWorkflowFactory">
<property key="resource" value="workflows.xml" />
</factory>
</osworkflow>这个例子指明了我们准备使用内存 (MemoryWorkflowStore) 来保存流程数据。这样可以减少设置数据库的相关信息,减少出问题的可能性。用内存持久化对于测试来说是非常方便的。
Workflow factories
上面的配置文件还指明了我们工作流工厂(XMLWorkflowFactory),工作流工厂的主要功能是管理流程定义文件,包括读取定义文件和修改定义文件的功能。通过'resource'这个属性指明了采用通过从classpath中读取流程定义文件的方式,按照这个定义,接下来我们需要在classpath中创建一个名为workflows.xml的文件。
workflows.xml 的内容:
<workflows>
<workflow name="mytest" type="resource" location="myworkflow.xml"/>
</workflows>我们把 myworkflow.xml 和workflows.xml放在同一目录,这样它就能够被工作流工厂读取了。
这样就完成了配置,接下来是初始化一个流程并调用它。
Initialising OSWorkflow
OSWorkflow 的调用模式相当简单:通过一个主要的接口来执行大部分操作。这个接口就是 Workflow interface,及其扩展 AbstractWorkflow 的实现,例如EJBWorkflow 和 SOAPWorkflow. 为了简单起见,我们使用最基本的一种: BasicWorkflow。
首先,我们来创建Workflow。在实际项目中,这个对象应该被放在一个全局的位置上以供重用,因为每次都创建一个新的Workflow对象是需要耗费比较昂贵的系统资源。在这里的例子,我们采用BasicWorkflow,它的构建器由一个当前调用者的用户名构成,当然我们很少看到单用户的工作流应用,可以参考其他的Workflow实现有不同的方式去获得当前调用者。
为了简单起见,我们采用BasicWorkflow来创建一个单一的用户模式,避免编写其他获取用户方法的麻烦。 0
这样我们来创建一个'testuser'调用的workflow:
Workflow workflow = new BasicWorkflow("testuser");下一步是提供配置文件,在大多数情况下,只是简单的传递一个DefaultConfiguration实例:
DefaultConfiguration config = new DefaultConfiguration();
workflow.setConfiguration(config);现在我们已经创建并且配置好了一个workflow,接下来就是开始调用它了。
启动和进行一个工作流程
首先我们需要调用initialize 方法来启动一个工作流程,这个方法有3个参数,workflow name (定义在workflows.xml里,通过workflow factory处理), action ID (我们要调用的初始化动作的ID),和初始化变量。 因为在例子里面不需初始化变量,所以我们只是传递一个null,
long workflowId = workflow.initialize("mytest", 1, null);我们现在已经有了一个工作流实例,返回的workflowId可以在后续的操作中来代表这个实例。这个参数会在Workflow interface的绝大部分方法中用到。
检验工作流
现在让我们来检验启动的工作流实例是否按照我们所预期的那样运行。根据流程定义,我们期望的当前步骤是第一步,而且应该可以执行第一个动作(开始编写草稿)。
Collection currentSteps = workflow.getCurrentSteps
(workflowId);
//校验只有一个当前步骤
assertEquals("Unexpected number of current steps",
1, currentSteps.size());
//校验这个步骤是1
Step currentStep = (Step)currentSteps.iterator().next();
assertEquals("Unexpected current step", 1,
currentStep.getStepId());
int[] availableActions =
workflow.getAvailableActions(workflowId);
//校验只有一个可执行的动作
assertEquals("Unexpected number of available actions", 1,
availableActions.length);
//校验这个步骤是1
assertEquals("Unexpected available action", 1, availableActions[0]);执行动作
现在已经校验完了工作流实例正如我们所期望的到了第一个步骤,让我们来开始执行第一个动作:
workflow.doAction(workflowId, 1, null);这是简单的调用第一个动作,工作流引擎根据指定的条件,改变状态到‘Underway’,并且保持在当前步骤。
现在我们可以类似地调用第2个动作,它所需要的条件已经都满足了。
在调用完第2个动作以后,根据流程定义就没有可用的动作了,getAvailableActions将会返回一个空数组。
Congratulations, 你已经完成了一个工作流定义并且成功地调用了它。下一节我们将会讲解osworkflow一些更深入的概念。
3. Further descriptor concepts
定义条件和函数
你也许已经注意到,到目前为止,我们定义的条件和函数类型都是“class”。这种类型的条件和函数接受一个参数:“class.name”,以此来指明一个实现FunctionProvider或Condition接口的完整类名。
在osworkflow里面也有一些其他内置的类型,包括beanshell,无状态的session bean,JNDI树上的函数等。我们在下面的例子里使用beanshell类型。
Property sets
我们可能需要在工作流的任意步骤持久化一些少量数据。在osworkflow里,这是通过OpenSymphony的PropertySet library来实现。一个PropertySet基本上是一个可以持久化的类型安全map,你可以添加任意的数据到propertyset(一个工作流实例对应一个propertyset),并在以后的流程中再读取这些数据。除非你特别指定操作,否则propertyset中的数据不会被清空或者被删除。任意的函数和条件都可以和propertyset交互,以beanshell script来说,可以在脚本上下文中用“propertyset”这个名字来获取。下面来看具体写法是怎么样的,让我们增加如下的代码在“Start First Draft”动作的pre-functions里面:
<function type="beanshell">
<arg name="script">propertySet.setString("foo", "bar")</arg>
</function>这样我们就添加了一个持久化的属性“foo”,它的值是“bar”。这样在以后的流程中,我们就可以获得这个值了。
Transient Map 临时变量
另外一个和propertyset变量相对的概念是临时变量:“transientVars”。临时变量是一个简单的map,只是在当前的工作流调用的上下文内有效。它包括当前的工作流实例,工作流定义等对应值的引用。你可以通过FunctionProvider的javadoc来查看这个map有那些可用的key。
还记得我们在教程的第2部分传入的那个null吗?如果我们不传入null的话,那么这些输入数据将会被添加到临时变量的map里。
inputs 输入
每次调用workflow的动作时可以输入一个可选的map,可以在这个map里面包含供函数和条件使用的任何数据,它不会被持久化,只是一个简单的数据传递。
Validators 校验器
为了让工作流能够校验输入的数据,引入了校验器的概念。一个校验器和函数,条件的实现方式非常类似(比如,它可以是一个class,脚本,或者EJB)。在这个教程里面,我们将会定义一个校验器,在“finish first draft”这个步骤,校验用户输入的数据“working.title”不能超过30个字符。这个校验器看起来是这样的:
package com.mycompany.validators;
public class TitleValidator implements Validator
{
public void validate(Map transientVars, Map args,
PropertySet ps)
throws InvalidInputException, WorkflowException
{
String title =
(String)transientVars.get("working.title");
if(title == null)
throw new InvalidInputException("Missing working.title");
if(title.length() > 30)
throw new InvalidInputException("Working title too long");
}
}然后通过在流程定义文件添加validators元素,就可以登记这个校验器了:
<validators>
<validator type="class">
<arg name="class.name">
com.mycompany.validators.TitleValidator
</arg>
</validator>
</validators>这样,当我们执行动作2的时候,这个校验器将会被调用,并且检验我们的输入。这样在测试代码里面,如果加上:
Map inputs = new HashMap();
inputs.put("working.title",
"the quick brown fox jumped over the lazy dog," +
" thus making this a very long title");
workflow.doAction(workflowId, 2, inputs);我们将会得到一个InvalidInputException,这个动作将不会被执行。减少输入的title字符,将会让这个动作成功执行。
我们已经介绍了输入和校验,下面来看看寄存器。
Registers 寄存器
寄存器是一个工作流的全局变量。和propertyset类似,它可以在工作流实例的任意地方被获取。和propertyset不同的是,它不是一个持久化的数据,而是每次调用时都需要重新计算的数据。
它可以被用在什么地方呢?在我们的文档管理系统里面,如果定义了一个“document”的寄存器,那么对于函数、条件、脚本来说就是非常有用的:可以用它来获得正在被编辑的文档。
寄存器地值会被放在临时变量(transientVars map)里,这样能够在任意地方获得它。
定义一个寄存器和函数、条件的一个重要区别是,它并不是依靠特定的调用(不用关心当前的步骤,或者是输入数据,它只是简单地暴露一些数据而已),所以它不用临时变量里的值。
寄存器必须实现Register接口,并且被定义在流程定义文件的头部,在初始化动作之前。
举例来说,我们将会使用一个osworkflow内置的寄存器:LogRegister。这个寄存器简单的添加一个“log”变量,能够让你使用Jakarta的commons-logging输出日志信息。它的好处是会在每条信息前添加工作流实例的ID。
<registers>
<register type="class" variable-name="log">
<arg name="class.name">
com.opensymphony.workflow.util.LogRegister
</arg>
<arg name="addInstanceId">true</arg>
<arg name="Category">workflow</arg>
</register>
</registers>这样我们定义了一个可用的“log”变量,可以通过其他的pre-function的脚本里面使用它:
<function type="beanshell">
<arg name="script">transientVars.get("log").info("executing action 2")
</arg>
</function>日志输出将会在前面添加工作流实例的ID
结论
这个教程的目的是希望可以阐明一些主要的osworkflow概念。你还可以通过API和流程定义格式去获取更多的信息。有一些更高级的特性没有在此提到,比如splits 分支、joins 连接, nested conditions 复合条件、auto stpes 自动步骤等等。你可以通过阅读手册来获得更进一步的理解。
[quote] 以上文件都找自互联网,转载申明。[/quote]
osworkflow 入门基础的更多相关文章
- osworkflow 入门基础2
[quote]前篇我引入了互联网上找来的一篇文章,接着上篇讲:osworkflow 工作流是非常轻量级的,虽然2006就停止活动了,互联网上的资料也不是很多,官网也没过多的说明,比起jbpm 和act ...
- mybatis入门基础(二)----原始dao的开发和mapper代理开发
承接上一篇 mybatis入门基础(一) 看过上一篇的朋友,肯定可以看出,里面的MybatisService中存在大量的重复代码,看起来不是很清楚,但第一次那样写,是为了解mybatis的执行步骤,先 ...
- 01shell入门基础
01shell入门基础 为什么学习和使用shell编程 shell是一种脚本语言,脚本语言是相对于编译语言而言的.脚本语言不需要编译,由解释器读取程序并且执行其中的语句,而编译语言需要编译成可执行代码 ...
- Markdown入门基础
// Markdown入门基础 最近准备开始强迫自己写博文,以治疗严重的拖延症,再不治疗就“病入骨髓,司命之所属,无奈何”了啊.正所谓“工欲善其事,必先利其器”,于是乎在写博文前,博主特地研究了下博文 ...
- JavaScript入门基础
JavaScript基本语法 1.运算符 运算符就是完成操作的一系列符号,它有七类: 赋值运算符(=,+=,-=,*=,/=,%=,<<=,>>=,|=,&=).算术运 ...
- C++ STL编程轻松入门基础
C++ STL编程轻松入门基础 1 初识STL:解答一些疑问 1.1 一个最关心的问题:什么是STL 1.2 追根溯源:STL的历史 1.3 千丝万缕的联系 1.4 STL的不同实现版本 2 牛刀小试 ...
- HTML入门基础教程相关知识
HTML入门基础教程 html是什么,什么是html通俗解答: html是hypertext markup language的缩写,即超文本标记语言.html是用于创建可从一个平台移植到另一平台的超文 ...
- Linux shell入门基础(六)
六.Shell脚本编程详解 将上述五部分的内容,串联起来,增加对Shell的了解 01.shell脚本 shell: # #perl #python #php #jsp 不同的脚本执行不同的文本,执行 ...
- Linux shell入门基础(一)
Linux shell入门基础(一): 01.增加删除用户: #useradd byf userdel byf(主目录未删除) userdel -r byf 该用户的属性:usermod 用 ...
随机推荐
- H3C 模拟器 防火墙开启Web功能
最近在搞运维的一些事情,由于缺少实体的机器来进行操作,先在模拟器里面进行 环境 windows 10,模拟器 HCL_V2.1.1 防火墙 1 在windows添加虚拟网卡 我的电脑--管理--设备管 ...
- 如何把本地文件上传至github?
(都说git好用,但我觉得git把我弄得像个git……在反反复复用git bash的命令行上传失败了N次之后,终于可以用命令行把文件上传到GitHub了 这中间,还要感谢网络上的各种git教程!!!) ...
- Ubuntu18.04两步纯小白傻瓜无脑式安装Caffe
前言 Ubuntu16安装caffe过于繁琐,然而Ubuntu18安装起来却仅仅两步而已 附上官方安装教程:http://caffe.berkeleyvision.org/install_apt.ht ...
- VMware Workstation 15 Pro简化安装Kali Linux 2019.2
记录下简单安装的步骤
- 【python基础学习】---解析多层json,解析xml
1.以豆瓣的API接口为例子,解析返回的json数据 https://api.douban.com/v2/book/1220502 { "rating":{ "max&q ...
- 字符串转 Boolean 的正确方式
String s1 = "false"; String s2 = "true"; String s3 = "fAlSe"; String s ...
- 吉首大学2019年程序设计竞赛(重现赛)-K(线段树)
题目链接:https://ac.nowcoder.com/acm/contest/992/K 题意:给一个大小为1e5的数组,由0 1组成,有两种操作,包括区间修改,将一段区间内的0换成1,1换成0; ...
- 云数据库 MariaDB 版
基于MariaDB企业版全球独家合作认证,提供Oracle兼容性及众多企业级数据库特性.支持包括MySQL InnoDB等多种存储引擎,为不同需求的用户提供灵活的选择. 请看视频简介 优势 Oracl ...
- 什么是云数据库RDS PostgreSQL 版
PostgreSQL被业界誉为“最先进的开源数据库”,面向企业复杂SQL处理的OLTP在线事务处理场景,支持NoSQL数据类型(JSON/XML/hstore).支持GIS地理信息处理. 优点 NoS ...
- 解决jsp无法用el表达式的问题
在写springMVC的项目时,用jsp去前端展示数据,期间遇到了一个问题就是无法用el表达式. 最后排除一切之后发现是因为自己maven项目里web.xml的版本问题. 我的maven项目web.x ...