示例介绍
本文的示例实现对员工信息的增删查改等基本功能。用 Tapestry 实现表示层,用 Hibernate 开发持久层,用 SPRing 提供事务控制等跨模块服务,并用 Acegi 进行安全管理。本示例只用到一个域模型:Employee,下面是它的 UML 图。
图 1. Employee UML 图
搭建开发环境
本文的代码开发平台采用的是 Windows 操作系统,因此,以下环境设置也是针对 Windows 操作系统的。
从 AppFuse 下载页面 下载 appfuse-tapestry-1.9.3-src.zip,并解压缩在任意目录下。这个 zip 已经定制了使用 Tapestry 作为表现层的实现框架,因而使用起来较为直接。
从 http://java.sun.com 下载最新的 JDK,并安装或解压缩到任意目录下。本文采用 JDK 1.5.0。设置环境变量 JAVA_HOME 指向 JDK 所在的目录,并在 PATH 中添加 %JAVA_HOME%/bin。
从 http://jakarta.apache.org/tomcat 下载最新版的 Tomcat,并安装或解压缩到任意目录下。本文采用 Tomcat 5.5.17。设置环境变量 CATALINA_HOME 指向 Tomcat 的安装目录。
从 http://ant.apache.org下载最新版的 Ant,并解压缩到任意目录下。AppFuse 要求的最低版本是 1.6.2,本文采用的是 1.6.5。设置 ANT_HOME 指向 Ant 所在的目录,并在 PATH 中添加 %ANT_HOME%/bin。另外,要拷贝一个 junit.jar 到 %ANT_HOME%/lib 下,如果 lib 下没有 junit.jar,AppFuse 的脚本在运行时会给出警告信息。junit.jar 可以从 http://www.junit.org 获得,也可以从 %AppFuse%/lib/junit3.8.1 目录下获得。
从 http://www.MySQL.com 下载最新版的 MySQL,并安装或解压缩到任意目录下。本文采用的是 5.0。
从 http://www.eclipse.org 下载 Eclipse 3.1 或 3.2,安装到任意目录下。
AppFuse 的 Ant 脚本可以在命令行中运行,也可以在 Eclipse 里运行。有关如何在 Eclipse 里执行 Ant 脚本,请参考《用Eclipse开发AppFuse应用》。到此,我们已经为 AppFuse 开发应用准备好了环境,下面让我们开始使用 AppFuse 创建项目。
新建项目
AppFuse 的便捷与强大之处在于它已经为我们提供了多种开源框架的集成,并且通过使用 Ant 将所有的构建过程自动化。另外,AppFuse 利用 XDoclet 能够为我们生成绝大多数重要的代码,例如 dao 类、service 类以及测试用例,等等,并且能够将大量的配置文件也一并生成好,从而极大地节省了开发人员的时间。
用 AppFuse 进行开发通常有三种模式:“自上而下”,“自下而上”以及“混合模式”。采用“自上而下”(由 Java 对象向数据库对象创建的过程)的方式固然比较符合“面向对象”的设计思维,但是为此要编写大量的 XDoclet 的 tag 也确是一件痛苦的事情。相比较而言,采用“自下而上”(由数据库对象生成 Java 对象的过程)就显得简单许多 -- 只需要提供数据库表结构。然而,对于较为复杂的系统,尤其是类之间具有大量的关联的情形,仍然需要采用“自上而下”的创建模式。因此,在实际的项目开发中,将两种模式进行混合使用比较常见,这也就是“混合”模式。本文采用“自下而上”的模式。
本文的 AppFuse 安装在 "c:\opt" 下面。打开命令行控制台,进入 "c:\opt\appfuse",运行 “ant new”,为简单起见,所有参数选用默认值,见图 2。
图 2. ant new -- 新建项目
脚本运行成功后,新项目创建在 c:\opt\myapp 下(与 AppFuse 目录同级),myapp 是 AppFuse 默认的项目名称。将该项目导入到 Eclipse 中,并根据 《用Eclipse开发AppFuse应用》 进行必要的设置。以下是两个你可能需要进行的配置:
AppFuse 默认连接 MySQL 的用户名是 root,密码为空。如果你的 root 密码不是空,需要修改 C:\opt\myapp\build.properties 中的 database.admin.passWord 项,记得将注释去掉。
AppFuse 默认不是用 utf-8 创建数据库,如果你需要支持多语言,需要修改 C:\opt\myapp\metadata\sql\mysql-create.sql 中的创建数据库的语句,修改如下:
清单 1. 创建数据库语句
create database if not exists @DB-NAME@ CHARACTER SET utf8 COLLATE utf8_general_ci;
注:AppFuse 会在构建期将 @DB-NAME@ 替换成你指定的数据库名(本文中为“mydb”)。
在 c:\opt\myapp 下运行“ant setup test-all”。“setup” 完成了很多“设置”工作:创建数据库、构建 dao 和 serive 类、加载样本数据、创建 war 文件并部署到 tomcat,等等。“test-all” 运行所有的测试用例:对 dao,service 以及页面的测试。如果这个脚本运行成功,说明开发环境一切就绪。这时,启动 Tomcat,通过访问 http://localhost:8080/myapp 就能够看到 AppFuse 的登录界面了。AppFuse 预定义了两个用户:mraible 和 tomcat,密码都是 tomcat。mraible 属于管理员角色(能够管理用户信息),tomcat 属于普通用户角色。用 mraible 登录可以看到 图 3的界面。
图 3. AppFuse 的初始界面
或许此时,你已惊奇地发现,自己不过只运行了一次 Ant 脚本,但是系统已经拥有“用户管理”、“邮件”、“文件上传” 等功能 -- 这就是 AppFuse “开箱即用”的优势。接下来让我们开始开发前述的应用示例。
创建数据库表
在 mydb 数据库中执行如下语句创建 employee 表:
清单 2. 创建 employee 语句
CREATE TABLE `employee` (
`id` bigint(20) NOT NULL auto_increment,
`code` varchar(10) NOT NULL,
`dept` varchar(50) NOT NULL,
`name` varchar(20) NOT NULL,
`status` varchar(10) NOT NULL,
`telephone` varchar(20) default NULL,
`title` varchar(50) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
用 AppGen 生成代码
AppFuse 自带了一个代码生成工具 -- AppGen,它位于 c:\opt\myapp\extras\appgen 目录下面。AppGen 可以生成绝大部分我们需要的代码,比如 dao 类,service 类,菜单、增删改的 web 页面、配置文件、样本数据,等等。AppGen 利用 XDoclet 生成代码,因此可以在 extras/appgen/src 看到很多 .xdt 文件,这些就是 XDoclet 的模版定义文件。如果你希望自己编写 dao 和 service 类,就运行“install”这个 target,否则就用 “install-detailed” ,它可以帮你搞定一切。下面就让我们来运行 “install-detailed” 生成代码。在 c:\opt\myapp\extras\appgen 下运行 “ant install-detailed”。
清单 3. 运行 install-detailed
...
[input] Would you like to generate code from a table or POJO? (table,pojo)
table
[input] What is the name of your table (i.e. person)?
employee
[input] What is the name, if any, of the module for your table (i.e. organization)?
hr
...
前两个问题都很直观:选择从 table 生成代码,表名是 employee。第三个问题是让用户输入使用的模块名,如果你希望 AppFuse 帮你按模块生成代码的话,就需要输入一个模块名称。这里,我们输入“hr”。如果运行成功,在 Eclipse 中会看到如下的目录结构:
图 4. “install-detailed” 执行后的 Eclipse
表 2 列出了 "install-detailed" 生成的主要文件。
表 2. "install-detailed" 生成的主要文件列表
文件
说明
myapp/src/dao/org/appfuse/dao/hibernate/applicationContext-hibernate.xml
在其中增加了 employeeDao 的声明
myapp/src/dao/org/appfuse/hr/model/Employee.java
Employee 类 -- Java Bean
myapp/build/dao/gen/org/appfuse/hr/model/Employee.hbm.xml
Employee 类的 Hibernate 映射文件
myapp/src/dao/org/appfuse/hr/dao/EmployeeDao.java
定义关于 employee 的 dao 层的操作
myapp/src/dao/org/appfuse/hr/dao/hibernate/EmployeeDaoHibernate.java
EmployeeDao 的 Hibernate 实现类
myapp/src/service/org/appfuse/service/applicationContext-service.xml
在其中增加了employeeManager的声明
myapp/src/service/org/appfuse/hr/service/EmployeeManager.java
定义关于 employee 的 service 层的操作
myapp/src/service/org/appfuse/hr/service/impl/EmployeeManagerImpl.java
EmployeeManager 的实现类
myapp/src/web/org/appfuse/hr/webapp/action/EmployeeForm.java
employee 的添加/修改页面对应的 tapestry 类
myapp/src/web/org/appfuse/hr/webapp/action/EmployeeList.java
employee 的列表页面对应的 tapestry 类
myapp/test/dao/org/appfuse/hr/dao/EmployeeDaoTest.java
employee dao 类的测试用例
myapp/test/service/org/appfuse/hr/dao/EmployeeManagerTest.java
employee service 类的测试用例
myapp/test/web/org/appfuse/hr/webapp/action/EmployeeFormTest.java
employee 添加/修改页面类的测试用例
myapp/test/web/org/appfuse/hr/webapp/action/EmployeeFormTest.java
employee 列表页面类的测试用例
myapp/web/pages/hr/employeeForm.Html
employee 添加/修改页面 html 模版文件
myapp/web/pages/hr/employees.html
employee 列表页面 html 模版文件
myapp/web/pages/hr/employeeForm.page
employee 添加/修改页面规格文件
myapp/web/pages/hr/employees.page
employee 列表页面规格文件
不过,AppFuse 并不知道开发者需要加载哪些 hbm 文件,所以要手工将 Employee.hbm.xml 文件添加到配置文件中:打开 applicationContext-hibernate.xml,在 “sessionFactory” 的 bean 声明中,找到 “mappingResources” 属性的定义,增加 “<value>org/appfuse/hr/model/Employee.hbm.xml</value>”。
清单 4. applicationContext-hibnerate.xml 中添加 Employee.hbm.xml
...
<beans>
<!-- Hibernate SessionFactory -->
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
-- XML error: The previous line is longer than the max of 90 characters --
<property name="dataSource" ref="dataSource"/>
<property name="mappingResources">
<list>
<value>org/appfuse/hr/model/Employee.hbm.xml</value>
<value>org/appfuse/model/Role.hbm.xml</value>
<value>org/appfuse/model/User.hbm.xml</value>
</list>
</property>
...
在 c:\opt\myapp 下运行 “ant deploy”。打开 “http://localhost:8080/myapp”,用 mraible/tomcat 登录,“Employee List” 已经被添加到菜单里了。
图 5. myapp 的原始主页面
点击 “Employee List” 链接,进入“员工信息列表”页面。
图 6. myapp 的原始员工信息列表页面
点击“添加”按钮或点击任意一行数据,进入“员工信息添加/修改/删除”页面。
图 7. myapp 的原始员工信息添加/修改/删除页面
不难看出,虽然 AppFuse 帮我们生成了页面,但是这些页面并非那么“理想”,我们仍然需要根据实际的需求做些调整。
根据项目需求调整代码
在本文中,做了如下代码修改:
将所有页面文字翻译成中文:AppFuse 中用到的所有 Resource Bundle 文件位于 myapp/web/WEB-INF/classes 目录下(以ApplicationResources开头的properties文件)。更改 ApplicationResources_zh_CN.properties 的文件编码方式为“UTF-8”。然后,把 ApplicationResources.properties 中 “# -- Employee-START” 和 “# -- Employee-END” 之间的项拷贝到 ApplicationResources_zh_CN.properties 中,并逐项翻译成中文。AppFuse 会在脚本运行的时候自动用 native2ascii 进行编码转换。另外,AppFuse 默认对 “button.done” 的翻译是“做”,这不太合适,所以改为“完成”。
在“员工信息列表页面”去掉了 id 列,并调整了列的顺序:只要修改 employees.html 就可以。
在“员工信息添加/修改/删除页面”,将“所在部门”、“职位”、“状态”改为下拉列表:需要修改 employeeForm.html、employeeForm.page、EmployeeForm.java。用 PropertySelection 组件实现下拉列表,用 Resource Bundle 文件定义真正显示的选项文本。
增加了一个“人事管理”的角色,用来执行员工信息管理的权限控制:具体介绍见“系统安全”。
添加了一个新的主题 “mytheme”(只是更改了界面的颜色):具体介绍见“页面布局和样式”。
应用了上述修改后,在 c:\opt\myapp 中运行 “ant deploy” 重新打包整个项目并发布。以下是修改后的界面截图:
图 8. 修改后的 myapp 主页面
图 9. 修改后的 myapp 员工信息列表页面
图 10. 修改后的 myapp 员工信息添加/修改/删除页面
图 11. 修改后的 myapp 用户管理页面
其他功能
一个系统除了包含核心逻辑之外,还有其他一些辅助功能,它们也是非常重要的。下面,让我们来看看如何在 AppFuse 中开发这些功能。
语言国际化
如果你的系统不仅仅支持一种语言,那么就需要考虑这个问题。在 AppFuse 中,Resource Bundle 文件是位于 web\WEB-INF\classes 目录下的以 ApplicationResources 开头的 properties 文件。Tapestry 有自己的国际化文本机制。但是在 AppFuse 中,并不全是 Tapestry 页面,仍有些地方使用 jsp,而这些页面使用 JSTL 的 fmt 标签显示国际化文本。不过,AppFuse 已经将这二者的“源头”进行了整合,因此,对用户而言,只需要在 ApplicationResources*.properties 中定义需要国际化的文本。
但是,在 Eclipse 中可以看到,AppFuse 的 properties 文件默认的编码不是 UTF-8,而是 ISO-8859-1,这样会导致最后通过 native2ascii 转换后的文件都是 “???”,所以用户需要自己把这些文件转成 UTF-8。转换的方法很简单:在 properties 文件上点右键,在右键菜单上选择 Properties,打开属性窗口后,更改 “Text file encoding” 为 UTF-8。在修改编码前,最好先把已有的文字拷贝出来,转换好之后再粘贴回去,否则会导致原先翻译好的文字变成乱码。
图 12. ApplicationResources_zh_CN.properties 的属性窗口
AppFuse 在发布项目的时候,会自动用 native2ascii 转换这些资源文件。如果你想使用其他资源文件名,可以修改 web\WEB-INF\web.xml 中的 “javax.servlet.jsp.jstl.fmt.localizationContext” 的参数值。
页面布局和样式
使用 AppFuse,能够很方便的修改系统的整体布局和样式,因为 AppFuse 使用了一种强大的 “CSS框架”。项目创建好之后,在 web\styles 目录下,有三个目录:andreas01,puzzlewithstyle 和 simplicity。这些是 AppFuse 自带的三种主题,目录名即 CSS 框架的主题名。属于“管理员”角色的用户可以在登录后通过在 url 后面添加形如 "?theme=andreas01" 的参数更改系统使用的主题。如下图:
图 13. 应用了 “puzzlewithstyle” 的 myapp
系统默认使用的主题由 web\WEB-INF\web.xml 中的 “theme” 参数指定,AppFuse 默认使用的主题是 “simplicity”。更改或创建新的主题也很简单,只要在 web\styles 目录下,新建一个自己的目录,并参照已有主题的编写规范定义自己的主题。本文中,拷贝了 simplicity 目录,更名为 “mytheme”,然后将里面的字体颜色从“蓝色”基调改成了“绿色”基调,并修改 web.xml 中的 theme 参数值为 “mytheme”,这样 myapp 默认使用的就是 mytheme 的主题了,如图 8所示。你也可以从 http://css.appfuse.org/themes/ 得到更多关于 “CSS框架” 的信息。
系统安全
AppFuse 使用 Acegi 进行安全管理。Acegi 的配置信息位于 web\WEB-INF\classes\security.xml。事实上,Acegi 是被集成到 Spring 当中的,因此这个文件是 Spring 的配置文件格式。在 web\WEB-INF\web.xml 中,该文件被指定在应用启动前会被加载:
清单 5. web.xml 关于 Spring 配置文件的定义
...
<!-- Context Configuration locations for Spring XML files -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext-*.xml,/WEB-INF/security.xml</param-value>
</context-param>
...
本文关于系统安全的实现如下:
在数据库中增加新的角色“hr”:编辑 myapp\metadata\sample-data.xml 文件,增加如下黑体的部分:
清单 6. sample-data.xml 中角色 “hr” 的记录
...
<table name='role'>
<column>id</column>
<column>name</column>
<column>description</column>
<row>
<value>1</value>
<value>admin</value>
<value><![CDATA[Administrator role (can edit Users)]]></value>
</row>
<row>
<value>2</value>
<value>user</value>
<value><![CDATA[Default role for all Users]]></value>
</row>
<row>
<value>3</value>
<value>hr</value>
<value><![CDATA[Role for employee mangement]]></value>
</row>
</table>
...
AppFuse 使用 dbunit 加载样本数据到数据库中,sample-data.xml 为 dbunit 提供样本数据定义。修改完 sample-data.xml,在 c:\opt\myapp\ 下运行 “ant db-load”,样本数据被重新加载。这样,我们就在数据库中定义了一个新的角色记录 “hr”。
定义角色名称的中文显示文本:在 myapp/sr/web/webapp/action/UserForm.java 的方法 pageBeginRender 中找到如下代码:
// initialize drop-downs
if (getAvailableRoles() == null) {
List roles =
(List) getServletContext().getAttribute(Constants.AVAILABLE_ROLES);
setAvailableRoles(new RoleModel(roles));
}
将其做如下修改:
// initialize drop-downs
if (getAvailableRoles() == null) {
List roles =
(List) getServletContext().getAttribute(Constants.AVAILABLE_ROLES);
for(int i=0;i<roles.size();i++){
LabelValue role=(LabelValue) roles.get(i);
role.setLabel(getText("rolelabel_"+role.getValue()));
}
setAvailableRoles(new RoleModel(roles));
}
并在 web\WEB-INF\classes\ApplicationResources_zh_CN.properties 中增加角色名称的定义:
rolelabel_admin=系统管理员
rolelabel_user=普通用户
rolelabel_hr=人事管理
AppFuse 默认在用户管理界面上显示的角色的名称是表 role 中的名称,这样无论切换到何种语言,角色名称都是 “admin”、"user"、“hr” 等等,角色名称不能根据 Locale 用相应的语言显示。因此,本文将角色的名称用 Resource Bundle 文件定义,数据库中存储 “key” 值。修改后的效果见 图 10。
配置“安全策略”:在 web\WEB-INF\security.xml 的 bean "filterInvocationInterceptor" 声明中增加如下“黑体”的一行:
<bean id="filterInvocationInterceptor"
class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="objectDefinitionSource">
<value>
PATTERN_TYPE_APACHE_ANT
/clickstreams.jsp*=admin
/flushCache.*=admin
/passwordHint.html*=ROLE_ANONYMOUS,admin,user
/reload.*=admin
/signup.html*=ROLE_ANONYMOUS,admin,user
/users.html*=admin
/employees.html*=hr
/**/*.html*=admin,user
</value>
</property>
</bean>
“/employees.html*=hr” 的意思是:只有 hr 这个角色可以访问形如 “/employees.html*” 的 url。
将“员工信息维护”菜单关联到指定角色 hr:在 web\WEB-INF\menu-config.xml 中在 “EmployeeMenu” 的定义中增加 “roles='hr'”:
<!--Employee-START-->
<Menu name="EmployeeMenu" title="employeeList.title"
page="/employees.html" roles="hr"/>
<!--Employee-END-->
于是,“员工信息维护”的菜单入口只对属于“人事管理”角色的用户显示,对其他用户则隐藏。
分配角色 “hr” 给 tomcat:将“人事管理”角色分配给某一用户,例如 tomcat。则 tomcat能够看见并访问“员工信息维护”相关页面,而其他用户的界面上则没有“员工信息维护”这个菜单入口。并且,如果用户试图通过url访问employees.html的时候会看到如下页面:
图 14. “访问被拒绝”页面
图 14是 AppFuse 提供的默认“访问被拒绝”页面,你可以通过修改 web\403.jsp 把它定制成自己喜欢的页面。
事务控制
AppFuse 利用 Spring 的事务管理机制。Spring 可以以声明的方式,对方法进行事务控制,并且可以根据实际的需要,调整控制粒度。“声明方式”的好处在于:核心代码只需要关注业务逻辑,而将事务控制完全交由配置文件管理,一方面是核心代码简洁清晰,另一方面也便于进行集中配置管理。
事务控制一般是定义在 service 类的方法上的。AppFuse 的所有 service 类都声明在 src\service\applicationContext-service.xml 中,该文件中包含有一个 “tXProxyTemplate” bean 的声明,它定义了基本事务策略。其它的 service 类从 “txProxyTemplate” 继承,并可以“重写”事务策略。例如,AppFuse 对 userManager 的声明如下:
<!-- Transaction template for Managers, from:
http://blog.exis.com/colin/archives/2004/07/31/
concise-transaction-definitions-spring-11/ -->
-- XML error: The previous line is longer than the max of 90 characters --
<bean id="txProxyTemplate" abstract="true"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager" ref="transactionManager"/>
<property name="transactionAttributes">
<props>
<prop key="save*">PROPAGATION_REQUIRED</prop>
<prop key="remove*">PROPAGATION_REQUIRED</prop>
<prop key="*">PROPAGATION_REQUIRED,readOnly</prop>
</props>
</property>
</bean>
<!-- Transaction declarations for business services.
To apply a generic transaction proxy to
-- XML error: The previous line is longer than the max of 90 characters --
all managers, you might look into using the BeanNameAutoProxyCreator -->
<bean id="userManager" parent="txProxyTemplate">
<property name="target">
<bean class="org.appfuse.service.impl.UserManagerImpl">
<property name="userDao" ref="userDao"/>
</bean>
</property>
<!-- Override default transaction attributes b/c of UserExistsException -->
<property name="transactionAttributes">
<props>
<prop key="save*">PROPAGATION_REQUIRED,-UserExistsException</prop>
<prop key="remove*">PROPAGATION_REQUIRED</prop>
<prop key="*">PROPAGATION_REQUIRED,readOnly</prop>
</props>
</property>
<!-- This property is overriden in applicationContext-security.xml to add
method-level role security -->
<property name="preInterceptors">
<list>
<ref bean="userSecurityInterceptor"/>
</list>
</property>
</bean>
Spring 提供了大量的参数和选项使开发者能够灵活地管理事务。有关 Spring 使用方面的知识,请参阅 Spring 的文档。另外,《Spring in Action》也是一个不错的选择。
日志
AppFuse 集成了 Log4j 进行日志管理,log4j.properties 位于 web\WEB-INF\classes 目录下。AppFuse 已经在绝大多数基类(诸如,BasePage.java、BaseDaoHibernate.java 以及 BaseManager.java 等)中加入了如下用于输出日志的成员变量:
protected final Log log = LogFactory.getLog(getClass());
因此,开发者只需要在自己的代码中调用 log 的方法就可以了,例如:“log.debug("entered 'delete' method");”。
邮件
AppFuse 集成了 Spring 的发送邮件的功能。发送邮件需要用的参数,如主机、端口等信息在 web\WEB-INF\classes\mail.properties 中进行配置。和发送邮件相关的 bean 已经在 applicationContext-service.xml 中声明:mailEngine、mailSender、velocityEngine 以及 mailMessage。用户只需要在自己的类中 “注入” mainSender 的实例,就可以发送邮件了。具体使用方法,请参阅Spring的文档。
缓存
AppFuse 对缓存机制的支持源自 Hibernate 对缓存的支持。Hibernate 提供了对五种缓存机制的集成,AppFuse 默认提供了其中的两种:Ehcache 和 Oscache。开发者也可以根据需要自行添加和配置。Acegi 默认提供了对 Ehcache 支持的实现,所以 Ehcache 是较好的选择。ehcache.xml 和 oscache.properties 位于 web\WEB-INF\classes 中。
结束语
使用 AppFuse 创建 Web 应用,步骤非常简单,你只需要了解如何运行 Ant 就能够使用 AppFuse;使用 AppFuse 创建 Web 应用,非常快速,因为 AppFuse 已经帮我们完成大部分代码生成/集成/配置的工作;使用 AppFuse 创建 Web 应用,非常省力,因为 AppFuse 已经提供了很多“开箱即用”的功能。体验快速开发,从 AppFuse 开始。
关于作者
沈锐在 J2EE 项目开发方面有多年的经验,目前在 IBM CSDL 从事 IBM Workplace Dashboard Framework 产品的开发工作。他对 Java 的开源技术有着浓厚的兴趣,欢迎使用 shenrui@cn.ibm.com 与他交流。
(出处:http://www.knowsky.com)