JavaServer Faces 简介

类别:Java 点击:0 评论:0 推荐:

作者:Ed Burns 和 Justyna Horwat

JavaServer Faces 1.0 Framework 使您可轻松创建强大和动态的 Web 应用程序。市场上有许多可用的 Web 用户界面框架,但是 JavaServer Faces Technology 由于以下几种原因而脱颖而出:它是一个 Java Community Process 标准;它为 Web UI 编程引入了 JavaBeans 组件范例;并且它的构建采用了许多已有架构的优点。作为一个标准,许多工具供应商可以受益于严格定义且一致的行为。JavaServer Faces Technology 的设计理念是一开始就在工具内部使用。用户也可以受益于不同 JavaServer Faces 技术实现之间的一致且明确的行为。

在 JavaServer Faces Technology 中提供了很多有用的特性。需要着重介绍的一些主要功能包括:

Managed Bean 工具 验证工具 丰富且可扩展的组件库 插入式呈现工具包 对具体用户事件响应的导航 跨请求保留应用程序状态 转换模型

本文将简要介绍 JavaServer Faces Technology 的这些功能并且给出一些例子。
关于这些功能的详细信息和 JavaServer Faces Technology 提供的其他功能,请参考 J2EE  指南的 JavaServer Faces Technology 的相关章节(第 17-21 章)。

JavaServer Faces 应用程序实质上是一个运行在一个与 Java(TM) 2 Platform, Enterprise Edition (J2EE(TM) platform) 兼容的容器中的 Servlet 或者 Java Server Pages(JSP) 应用程序。这说明它需要 Java Servlet 2.3 和 JSP 1.2 或者更新的版本。 开始创建和配置一个 JavaServer Faces 应用程序的最好的方法是免费下载Java 2 Software Development Kit, Standard Edition (J2SE SDK) 1.4 或者更高版本。需要指出的是,JavaServer Faces Technology 不要求在应用程序中使用 JSP 页面,您可以自由选择直接使用 Servlets 或者其他模板技术。如果确实对 JSP 容器使用了 JavaServer Faces Technology,您将通过 JavaServer Faces 的客户组件标签从内建的 JavaServer Faces 内核和 HTML 组件库中受益。JavaServer Faces 组件代表像文本字段、表单、按钮、表格、复选框等的 Web 控件。

使用 JavaServer Faces Technology 创建一个 JSP 页面时,在服务器的内存中会建立一个组件树,每一个组件标签对应树中的一个 UIComponent 实例。该框架使用组件树来处理应用程序的请求并且创建一个呈现出的响应。当用户生成一个事件时,例如,点击了一个按钮,JavaServer Faces 生命周期即处理该事件并且产生适当的响应。这是一个对大多数图形用户界面编程的表单而言而常见的一种的范例(paradigm)。

FacesServlet 是进入 JavaServer Faces 框架的入口点。它处理请求处理生命周期并且用作一个前端控制器。JavaServer Faces Technology 也有保存重要请求信息的上下文的概念。上下文对象被称为 FacesContext 。在 JavaServer Faces Technology 生命周期的每个阶段上下文对象都被修改并且每次请求时都是有效的。

JavaServer Faces Technology 框架也有“值绑定”和“方法绑定表达式”的概念。如果熟悉了像 JSP Standard Tag Library (JSTL) 或者 JSP 2.0 这样的技术,您就已经熟悉了表达式语言的概念。JavaServer Faces Technology 绑定表达式使您可以容易地和底层数据模型交互。Character Combat 演示应用程序举例说明了如何使用“值绑定”从数据模型中提取值。

简单的 JavaServer Faces Technology 应用程序

本文包含一个简单的 JavaServer Faces 应用程序,它阐明了 JavaServer Faces Technology 的一些重要概念。为了理解该应用程序, 您应该已经熟悉了包括JSP、 Servlets 和标签库在内的基本的 J2EE Web 技术。

示例应用程序后的基本想法是让用户参加一个快速有趣的游戏。您是否想知道如果从《指环王》中取两个人物并使它们互相打斗会发生什么?示例应用程序以一种简单有趣的方式回答了这个问题。 该应用程序的名称是 Character Combat 。

Character Combat 的组成如下:

JSP 页面,带有表示 UI 的 JavaServer Faces 组件
容纳模型数据的 backing bean 应用程序配置文件指定: JavaServer Faces Controller Servlet Managed Bean 导航处理


上图显示了 Character Combat 演示应用程序中的页面流:

在首页面,用户可以进行如下操作: 添加更多字符 直接转到下一页面 在第二页面,用户可以进行如下操作: 返回到首页面并添加更多字符 选定战斗的第一个参与者 转到第三页面 在第三页面,用户可以进行如下操作: 返回到第二页面 选定战斗的第二个参与者 转到尾页面 在尾页面,用户可以进行如下操作: 查看战斗结果 返回到第三页面 重新开始演示应用程序

您将注意到该工作流程符合“向导”UI 设计模式。我们已经将本例的向导功能提取到一个您自己的应用程序的一个简单 bean 中。

运行应用程序

最新的 JavaServer Faces Technology 1.0 Framework 和其他所有运行时用到的附属程序已经集成到 Sun Java System Application Server Platform Edtion 8 中。您不需要在 Application Server 中进行任何额外的配置步骤来设置一个 JavaServer Faces web 应用程序。Application Server 是免费的,它包含了 J2EE 技术最新的实现。如果您下载了 J2EE 1.4 SDK, 其中已经包含了 Application Server。


如果编写自己的 JavaServer Faces web 应用程序,要使用 JavaServer Faces Technology 1.0 框架,唯一需要 您在应用程序服务器中做的事情是指定 FacesServlet 实例并映射您的 Web 应用程序的部署描述符。所有附属文件已经是容器的一部分,您不需要绑定任何额外的 JAR 文件。

要运行本文中讨论的示例程序,只需要简单地将所提供的配置 war 文件部署在应用程序服务器中就可以了。通过使用上下文名称 “jsf-characterCombat” 就可以使用应用程序。除了一个预先构建的 war 文件,还提供了演示源代码。在与源代码一起发行的 README 中您将找到如何配置和构建的指示。

通过遵照 java.net 上的关于新建 javaserverfaces 项目的 FAQ 中的指示,可以访问本演示的源代码:

https://javaserverfaces.dev.java.net/faq.html#Code_checkout。

一旦遵照了这些指示,就可以在下述地址浏览本文的源代码:
https://javaserverfaces-sources.dev.java.net/source/browse/javaserverfaces-sources/jsf-demo/characterCombat/ 演示应用程序构建模块 部署描述符

为了在 Web 应用程序中使用 JavaServer Faces 框架,需要在部署描述符中定义 FacesServlet 和一个 FacesServlet 映射。下面是一个例子:

<!-- Faces Servlet -->
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>


<!-- Faces Servlet Mapping -->
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.faces</url-pattern>
</servlet-mapping>

FacesServlet 实例作为 JavaServer Faces Technology 框架中的一个前端控制器。它处理所有与 JavaServer Faces Technology 相关的请求。

在上面的例子中使用了扩展名映射。使用扩展名映射,Web 容器会将所有请求发送到 Faces Servlet,以获取扩展名如“*.faces”的 页面。

您也可以使用前缀映射来映射到 FacesServlet 实例。例如,您可以使所有名字以“/faces/*”为前缀的 Web 页面通过 FacesServlet 实例。

应用程序配置

所有 JavaServer Faces Technology 的具体配置信息都将进入像 faces-config.xml 这样的应用程序配置文件中。在配置文件中可以定义 Managed Bean、Navigation Rule、Converter 和 Validator 等。

下面是配置文件中的一个条目的例子:

<faces-config>

<managed-bean>
<managed-bean-name>modelBean</managed-bean-name>
<managed-bean-class>
characterCombat.ModelBean
</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
</managed-bean>

</faces-config>

该条目在一个 bean 名称和类之间创建了一个映射。该映射由 Managed Bean 创建工具使用。“modelBean”第一次被引用时,将创建模型对象并将它保存到适当的作用域中。本文的后面讨论了 Model Bean bean。

还有更多的配置选项,在 JavaServer Faces Technology 1.0 规范您可以找到完整的配置选项集。

构建应用程序

在演示源代码发布中包含了一个预构建的 war 文件。您可以在 Web 容器中部署该 war 文件。

注意该 war 文件包含了一个 JSTL 1.1 实现。如果您的 Web 容器不支持 JSTL 1.1,那么您需要用一个 JSTL 1.0 实现重新构建 war 文件。

如果希望自己构建演示应用程序,您需要遵循如下步骤:

在试图建立演示应用程序之前,确保拥有所有的编译时附属文件。演示应用程序提供了一个“build.properties.sample”文件。复制该文件并将其重新命名为“build.properties”并且进行更改使之适合您的环境。
如果您正在使用 Application Server or the J2EE 1.4 SDK, JavaServer Faces Technology 1.0 Framework 和运行一个 JavaServer Faces 应用程序的所需的全部附属文件已经是您的环境的一部分了。唯一需要做的事情是在您的“build.properties”文件中修改 SJSAS PE 8.0 安装位置。
Character Combat 演示应用程序提供了一个 “build.xml”文件。该文件包含了一组建立演示应用程序 web 档案的规则和目标。要构建应用程序您还需要 Ant。Ant 已经是 SJSAS PE 8.0 发行的一部分,它位于<SJSAS_HOME>/bin/asant 中。
要使用 SJSAS PE 8.0 容器来建立演示应用程序,只需要在您的解包的 Character Combat demo 目录中调用“asant”,您的“build.xml” 和定制的“build.properties”文件也在这个目录中。 Character Combat 演示应用程序架构 Managed Bean

Managed Bean 只是具有一个 public 无参数构造函数的类,这与 JavaBeans 1.0 的方法命名约定是一致的,并且和使用 Managed Bean 工具的 Faces 应用程序也是一样的。Managed Bean 工具在 WEB-INF/faces-config.xml 文件中进行配置。在该文件中您可以放置任何数量的<managed-bean>声明,每个声明包含一个名字、一个类和作用域。在 Web 应用程序 自身中,您可以使用在 JavaServer Faces Expression Language 表达式中定义的 Bean 名称来引用 bean。您第一次引用 bean 时,它将被创建并放置在适当的作用域中。Managed Bean 是非常灵活的,它允许您通过指定属性来定制自己的 bean,这些属性包括 Java 数组、映射、列表和其他 Managed Bean。

Character Combat 实例有一个名为 ModelBean 的 backing bean。下面是使用 Managed Bean 工具在 WEB-INF/faces-config.xml 文件中定义 ModelBean bean 的方法:

<managed-bean>
<managed-bean-name>
modelBean
</managed-bean-name>
<managed-bean-class>
characterCombat.ModelBean
</managed-bean-class>
<managed-bean-scope>
session
</managed-bean-scope>
</managed-bean>

那个 bean 后来在 Web 应用程序的 JSP 页面中被引用。下面是一个如何使用 bean 的例子:

<h:inputText value="#{modelBean.customName}" />

其中<h:inputText/>是一个嵌套在表单中的文本字段组件。当表单被提交时,文本字段中的值将作为“customName” 属性保存在 ModelBean 中。“#{”和“}”指定使用一个“值绑定”表达式。该值绑定表达式告诉框架 “customNameThe” 是一个 JavaBean 属性,它在“modelBean”关键字下 的Model Data 中定义。根据 WEB-INF/faces-config.xml 配置文件,框架的 Managed Bean 工具知道了类映射的 bean 名,并且如果 bean 不存在就初始化该 bean。

对象模型

Character Combat 例子创建了一个 ModelBean 对象。与其他 JavaBeans 组件一样,ModelBean 包含了一组访问方法。它也预置了一个 默认的字符表,同时也存储用户定制的字符。

ModelBean 是使用 Managed Bean 创建工具创建的,并且由在视图中定义的 JavaServer Faces 组件通过名字引用。ModelBean 说明了如何使用 bean 来保存组件的值。

在本例中只支持添加新字符。通过更改模型和视图来支持该功能,对程序的良好扩展可以支持删除功能。

JSP 页面

JSP 页面为 Web 应用程序提供 UI。JavaServer Faces Technology 提供两个 JSP 标签库,它们将组件展现给页面的作者。您可以定制这些组件或者创建自己的组件。标准组件作为基本 HTML 4.01 组件呈现,使得 JavaScript 绝对最小。这确保您的页面在所有的浏览器中按期望的样子显示。如果您需要对其他呈现类型 (如 WML 或 SVG 等)的支持,JavaServer Faces Technology 包含了一个 RenderKit 的概念,它是一个软件模块,组件使用它可以将自己呈现到特定的客户设备类型中。

要使用包含组件的内建 JavaServer Faces 标签库,您需要在 JSP 页面中包含下面的指令:

<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>

注意您的 JavaServer Faces 页面需要所有的 JavaServer Faces 标签包含在 <f:view>...</f:view> 标签间,从而可以正确的建立组件树。

每一个 HTML 组件都可以使用样式表来定制。您可以指定一个一般的 styleClass 或者为组件设置特定的样式属性值。

下面是一些用来说明上面的概念的 Character Combat 应用程序中的一些例子,:

DataTable

DataTable UIComponent 可以处理几种不同类型的数据模型,其中包括 java.util.List 和java.sql.ResultSet。它提取数据并将数据显示在一个可定制的表格中。该组件也可以使用样式表来定制。

在 Character Combat 演示应用程序中,使用 List 作为底层数据模型。下面是一个代码片断:

<h:dataTable columnClasses="list-column-center,
list-column-center,
list-column-center,
list-column-center"
headerClass= "list-header"
styleClass= "list-background"
value= "#{modelBean.dataList}"
var= "character" >

<f:facet name="header">
<h:outputText value="List of Available Characters"/>
</f:facet>

<h:column>
<f:facet name="header">
<h:outputText value="Name"/>
</f:facet>
<h:outputText value="#{character.name}"/>
</h:column>

<h:column>
<f:facet name="header">
<h:outputText value="Species"/>
</f:facet>
<h:outputText value="#{character.species.type}"/>
</h:column>
...
</h:dataTable>

正如您在本例中所见到的,根据在<h:dataTable/>标签中的定义,#{modelBean.dataList} 对保存在 “character” 变量中的字符条目列表进行求值。对于列表中的每个字符都创建一个新行并且根据相应的 <h:column/> tags 标签进行显示。

<f:facet/> 标签在包含在 facet 中的组件和它的父组件之间创建了一种特殊的关系。这种特殊关系使您可定义组件为标题或者页脚。在我们的例子中,我们使用 facet 来为列创建标题。

既然 <h:dataTable/> 是一个 HTML 组件,它的样式就可以使用样式表来定制。在我们的例子中,我们为您展示如何为几种不同的 <h:dataTable/> 属性使用样式特性。我们在 JSP 页面标题中使用下面的代码片断来导入名为 “stylesheet.css” 的样式表:

<link rel="stylesheet" type="text/css"
href='<%= request.getContextPath() + "/stylesheet.css" %>'>

下面是呈现的 HTMLHere DataTable 的样子:

PanelGrid

对于简单的表格布局您可以使用<h:panelGrid/>组件。不像 DataTable 组件,PanelGrid 不接受任何底层数据模型。

下面是一个两列的表格的例子。在第一行定义了一个标题。第二行的第一列包含一个 InputText 字段,第二列包含了一个列表的下拉选项:

<h:panelGrid columnClasses="list-column-center,
list-column-center"
headerClass= "list-header"
styleClass= "inputList-background"
columns= "2">

<f:facet name="header">
<h:outputText value="Customize Character:"/>
</f:facet>
<h:inputText value="#{modelBean.customName}" />
<h:selectOneListbox value="#{modelBean.customSpecies}"
required="true" size="1" >
<f:selectItems value="#{modelBean.speciesOptions}"/>
</h:selectOneListbox>

</h:panelGrid> InputText

InputText 组件是一种用来得到用户提交的信息的方法。在我们的例子中,我们通过如下指定值属性将文本字段和模型连接起来:

<h:inputText value="#{modelBean.customName}" />

InputText 嵌套在一个表单中。一旦表单被提交,字段中的值就会映射到我们的模型中。

在下面的图像中,您可以看到 PanelGrid 布局组件中的一个 InputText、一个 CommandButtons 集合和一个 ListBox 的组合:

OutputText

OutputText 组件可以以不同的方式显示信息。例如,您可以将它配置为跳过 HTML 标签,更改所有尖括号的显示为适当的 &lt 语法或者无更改地直接传递 标签。 您也可以对该组件使用各种样式表。

在整个示例应用程序中都使用了 OutputText 来显示来自模型的数据。在本例中我们显示了字符的名字:

<h:outputText value="#{character.name}"/> SelectOneRadio

您可以使用 SelectOneRadio 来显示一个单选按钮选择集合。您可以包含一组单选选项和嵌套的选项组。下面是在我们的例子中使用 SelectOneRadio 的方法:

<h:selectOneRadio
layout="pageDirection"
required="true"
value="#{modelBean.firstSelection}">

<f:selectItems
value="#{modelBean.allCharactersToSelect}" />

</h:selectOneRadio>

Layout 属性告诉 <h:selectOneRadio/> 组件垂直布局而不是默认的水平布局。我们的 ModelBean“charactersToSelect” 方法返回一个 SelectItems 的列表,SelectOneRadio 知道如何显示它们。该单选组件嵌套在一个表单中。当表单被提交时,选中的单选项就会保存在模型的“currentSelection”属性中。

CommandButton

CommandButton 是一个输入组件,它可以创建 Action Event(动作事件)。您可以创建动作监听器来监听用户在浏览您的 JavaServer Faces web 应用程序时的特定的事件。您也可以为一个应用程序动作提供一个动作方法绑定,当选中组件时激活它。本例中我们使用了后者:

<h:commandButton actionListener="#{modelBean.addCustomName}"
value="Add Name"/>

在下一节我们将讨论动作,因为它们在导航处理中是非常重要的。

下面说明如何提交 RadioButtons 集合和 CommandButton 向导:

导航:示例向导组件让我们看一下该应用程序中处理导航的向导组件。该组件有三个部分,下图中用黑体显示它们。

要使用该组件用户必须做两件事情:

在 Web 应用程序 UI 中包含该组件 创造相应的导航规则

下面我们详细介绍这些步骤。WizardButtons  Managed Bean 的细节超出了本文的讨论范围,但是基本上讲,它有一些方法 可根据用户在向导页面流程中的当前位置来启用或禁用 next 和 back 按钮。

在 UI 中包含按钮 通过 <jsp:include> 机制可以在应用程序 UI 中包含组件。在示例应用程序的每个页面中,您都会在底部看到下面的代码行:



  <jsp:include page="wizard-buttons.jsp"/>

看一下该页面,我们看到它有下面的标签:

<%@ page contentType="text/html" language="java" %>
<%@ taglib prefix="f" uri="http://java.sun.com/jsf/core" %>
<%@ taglib prefix="h" uri="http://java.sun.com/jsf/html" %>

<f:subview id="wizard-buttons">

<h:panelGrid columns="2">

<h:commandButton value="< Back" action="back"
disabled="#{wizardButtons.hasBack}" />

<h:commandButton value="#{wizardButtons.nextLabel}"
action="next"
disabled="#{wizardButtons.hasNext}"/>

</h:panelGrid>

</f:subview>
值得注意的是任何位于被包含页面中的 JavaServer Faces 组件必须是 <f:subview> 标签的子组件,正如在父页面中它们必须是 <f:view> 标签的子页面一样。本例并没有展示这一点,但是您想包含在子视图中的任何模板标签文本都必须预先包装在 <f:verbatim> 标签内部。

您可以看到该页面只有一个 Panel Grid,它显示了两个相邻的按钮。这些按钮的属性被绑定到 WizardButtons bean 提供的方法和属性上。这些按钮和该 bean 被设计成协同工作。后退按钮有自己的值并且进行了动作硬编码。下一个按钮从 WizardButtons bean 提取值并且进行了动作硬编码。

导航规则

导航规则是向导组件的核心。这些规则必须包含在应用程序的 faces-config.xml 文件中。下面是演示应用程序规则的一个子集。您可以使用这些规则作为起点来在 您自己的应用程序中利用向导组件。

<navigation-rule>
<from-view-id>/main.jsp</from-view-id>
<navigation-case>
<description>
If the action returns "next", goto
firstSelection.jsp
</description>
<from-outcome>next</from-outcome>
<to-view-id>/firstSelection.jsp</to-view-id>
</navigation-case>
</navigation-rule>

<navigation-rule>
<from-view-id>/firstSelection.jsp</from-view-id>
<navigation-case>
<description>
If the action returns "next", goto
secondSelection.jsp
</description>
<from-outcome>next</from-outcome>
<to-view-id>/secondSelection.jsp</to-view-id>
</navigation-case>
<navigation-case>
<description>
If the action returns "back", goto
main.jsp
</description>
<from-outcome>back</from-outcome>
<to-view-id>/main.jsp</to-view-id>
</navigation-case>
</navigation-rule>

导航规则描述了如何根据动作的调用位置来处理动作,如下所示:

如果动作在 “"main.jsp” 页面调用并返回 “next”,则导航处理程序呈现 “firstSelection.jsp” 页面 如果动作在 “firstSelection.jsp” 页面调用并且返回“back”,则我们转到主页面 如果调用的动作返回 “next”,我们将转到 “secondSelection.jsp” 页面 动作

您可以回想到在工作流程图中首页面是特殊的,因为工作流程可以使得该页面重新加载。当用户希望向字符表中添加字符时就会重新加载首页面。这一添加是通过在 页面中放置一个按钮 来实现的,在按钮被按下时引发相应的动作。

在 “main.jsp” 页面中,您可以看到该按钮的声明:

<h:commandButton actionListener="#{modelBean.addCustomName}"
value="Add Name"/>

实际的动作处理程序的实现在 ModelBean.java 文件中。“addCustomName”方法将 name 添加到表格中。

public void addCustomName(ActionEvent event) throws
AbortProcessingException {

if ((customName != null) && (!customName.trim().equals(""))) {
customName = customName.trim();

//check to see if name already exists in list
Iterator iter = dataList.iterator();
while (iter.hasNext()) {
CharacterBean item = (CharacterBean) iter.next();
if (item.getName().equals(customName)) {
reset();
return;
}
}

//create new entry
CharacterBean item = new CharacterBean();
item.setName(customName);
item.setSpecies((SpeciesBean)
speciesPropertyMap.get(customSpecies));
dataList.add(item);
}
}

当调用带有动作监听器的 CommandButton 时,就会调用动作程序。“addCustomName”方法遍历已有字符的列表,如果没有在列表中发现新的名字,它就创建一个新的 Character 条目。

如果这个动作处理程序有一个返回值,导航处理程序可以用该值来决定下一个动作并根据结果来决定导航的方向。在这个特殊的例子中没有返回值,因为只是重新显示同一个页面,但是添加了一个新的用户名条目。动作 处理程序和导航处理程序的设计决定了它们可以轻松地互操作。

结束语

本文介绍了 JavaServer Faces Technology 框架中提供的一些功能。示例应用程序演示了组件、导航、动作处理程序和样式表的使用。

JavaServer Faces 可以帮助您轻松地创建复杂并且健壮的 Web 应用程序。它是一种在几年中发展起来的标准的 Java Web 应用程序框架。要开始使用 JavaServer Faces, 您需要做的就是免费下载 Sun Java System Application Server Platform Edition 8.0 或者 the J2EE 1.4 SDK。作为一个标准,它的定位是能够被用户和工具提供商所采用。JavaServer Faces Technology 是一个工具,它可以帮助您使用 MVC 原理构建出色的 Web 应用程序。作为一个结果,使用 JavaServer Faces,就可以区别什么是好的应用程序,什么又是出色的应用程序。

 

关于作者:

Ed Burns 是 Sun Microsystems 的高级工程师。自从 1994 年以来,Ed 参加了各种客户端和服务器端 Web 技术的工作,包括 NCSA Mosaic、 Mozilla、Sun Java Plugin、Jakarta Tomcat 以及最新的 JavaServer Faces。Ed 现在是 JavaServer Faces 的联合带头人。

Justyna Horwat 是 Sun Microsystems 的下一代 JSP 标准标签库的主任工程师。自从 1999 年以来,她广泛地参加了 Java Web 技术的研发,她同时对 JavaServer Faces 和 Apache Tomcat 对出了很大的贡献。几年来,她一直活跃在开放源代码社区,2002 年她成为 Apache 软件基金组织的第一位女性开发成员。

本文地址:http://com.8s8s.com/it/it12027.htm