使用 AppFuse 的七个理由

http://www.ibm.com/developerworks/cn/java/j-appfuse/

使用AppFuse快速构建J2EE应用http://www.ibm.com/developerworks/cn/java/j-lo-appfuse/

AppFuse是一个开放源码的项目和应用程序,它使用了在Java平台上构建的开放源码工具来帮助我们快速而高效地开发Web应用程序。我最初开发它是为了减少在为客户构建新Web应用程序时所花费的那些不必要的时间。从核心上来说,AppFuse是一个项目骨架,类似于通过向导创建新Web项目时IDE所创建的东西。当我们使用AppFuse创建一个项目时,它会提示我们将使用开放源码框架,然后才创建项目。它使用Ant来驱动测试、代码生成、编译和部署。它提供了目录和包结构,以及开发基于Java语言的Web应用程序所需要的库。

与大部分“newproject”向导不同,AppFuse创建的项目从最开始就包含很多类和文件。这些文件用来实现特性,不过它们同时也会在您开发应用程序时被用作示例。通过使用AppFuse启动新项目,我们通常可以减少一到两周的开发时间。我们不用担心如何将开放源码框架配置在一起,因为这都已经完成了。我们的项目都已提前配置来与数据库进行交互,它会部署到应用服务器上,并对用户进行认证。我们不必实现安全特性,因为这都早已集成了。

当我最初开发AppFuse时,它只支持Struts和Hibernate。经过几年的努力,我发现了比Struts更好的Web框架,因此我还添加了为这些Web框架使用的选项。现在,AppFuse可以支持Hibernate或iBATIS作为持久性框架。对于Web框架来说,我们可以使用JavaServerFaces(JSF)、SpringMVC、Struts、Tapestry或WebWork。

AppFuse提供了很多应用程序需要的一些特性,包括:

认证和授权

用户管理

RememberMe(这会保存您的登录信息,这样就不用每次都再进行登录了)

密码提醒

登记和注册

SSL转换

E-mail

URL重写

皮肤

页面修饰

模板化布局

文件上载

这种“开箱即用”的功能是AppFuse与其他CRUD代框架的区别之一(CRUD取自创建、检索、更新和删除几个操作的英文首字母),包括RubyonRails、Trails和Grails。上面提到的这些框架,以及AppFuse,都让我们可以从数据库表或现有的模型对象中生成主页/细节页。

图1阐述了一个典型AppFuse应用程序的概念设计:

图1.典型的AppFuse应用程序

清单1给出了我们在创建devworks项目时所使用的命令行交互操作,同时还给出了所生成的结果。这个项目使用了WebWork作为自己的Web框架(请参考下面参考资料一节给出的链接)。

清单1.使用AppFuse创建新项目

alotta:~/dev/appfusemraible$antnew

Buildfile:build.xml

clean:

[echo]Cleaningbuildanddistributiondirectories

init:

new:

[echo]

[echo]+-------------------------------------------------------------+

[echo]|--WelcometotheAppFuseNewApplicationWizard!--|

[echo]||

[echo]|Tocreateanewapplication,pleaseanswerthefollowing|

[echo]|questions.|

[echo]+-------------------------------------------------------------+

[input]Whatwouldyouliketonameyourapplication[myapp]?

devworks

[input]Whatwouldyouliketonameyourdatabase[mydb]?

devworks

[input]Whatpackagenamewouldyouliketouse[org.appfuse]?

com.ibm

[input]Whatwebframeworkwouldyouliketouse[webwork,tapestry,spring,js

f,struts]?

webwork

[echo]Creatingnewapplicationnamed'devworks'...

[copy]Copying359filesto/Users/mraible/Work/devworks

[copy]Copying181filesto/Users/mraible/Work/devworks/extras

[copy]Copying1fileto/Users/mraible/Work/devworks

[copy]Copying1fileto/Users/mraible/Work/devworks

install:

[echo]CopyingWebWorkJARsto../../lib

[copy]Copying6filesto/Users/mraible/Work/devworks/lib

[echo]AddingWebWorkentriesto../../lib.properties

[echo]AddingWebWorkclasspathentries

[echo]RemovingStruts-specificJARs

[delete]Deletingdirectory/Users/mraible/Work/devworks/lib/struts-1.2.9

[delete]Deletingdirectory/Users/mraible/Work/devworks/lib/strutstest-2.1.3

[echo]Deletingstruts_form.xdtforXDoclet

[delete]Deletingdirectory/Users/mraible/Work/devworks/metadata/templates

[echo]DeletingStrutsmerge-filesinmetadata/web

[delete]Deleting7filesfrom/Users/mraible/Work/devworks/metadata/web

[echo]DeletingunusedTagLibrariesandUtilities

[delete]Deleting2filesfrom/Users/mraible/Work/devworks/src/web/org/appfu

se/webapp

[echo]ModifyingappgenforWebWork

[copy]Copying12filesto/Users/mraible/Work/devworks/extras/appgen

[echo]Replacingsourceandtestfiles

[delete]Deletingdirectory/Users/mraible/Work/devworks/src/web/org/appfuse/

webapp/form

[delete]Deletingdirectory/Users/mraible/Work/devworks/src/web/org/appfuse/

webapp/action

[copy]Copying13filesto/Users/mraible/Work/devworks/src

[delete]Deletingdirectory/Users/mraible/Work/devworks/test/web/org/appfuse

/webapp/form

[delete]Deletingdirectory/Users/mraible/Work/devworks/test/web/org/appfuse

/webapp/action

[copy]Copying5filesto/Users/mraible/Work/devworks/test

[echo]Replacingwebfiles(images,scripts,JSPs,etc.)

[delete]Deleting1filesfrom/Users/mraible/Work/devworks/web/scripts

[copy]Copying34filesto/Users/mraible/Work/devworks/web

[delete]Deleting:/Users/mraible/Work/devworks/web/WEB-INF/validator-rules-c

ustom.xml

[echo]ModifyingEclipse.classpathfile

[echo]Refactoringbuild.xml

[echo]----------------------------------------------

[echo]NOTE:It'srecommendedyoudeleteextras/webworkasyoushouldn'tne

editanymore.

[echo]----------------------------------------------

[echo]Repackaginginfowrittentorename.log

[echo]

[echo]+-------------------------------------------------------------+

[echo]|--Applicationcreatedsuccessfully!--|

[echo]||

[echo]|Nowyoushouldbeabletocdtoyourapplicationandrun:|

[echo]|>antsetuptest-all|

[echo]+-------------------------------------------------------------+

BUILDSUCCESSFUL

Totaltime:15seconds

在创建一个新项目之后,我们就得到了一个类似于图2所示的目录结构。Eclipse和IntellijIDEA项目文件都是作为这个过程的一部分创建的。

图2.项目的目录结构

这个目录结构与Sun为Java2PlatformEnterpriseEdition(J2EE)Web应用程序推荐的目录结构非常类似。在2.0版本的AppFuse中,这个结构会变化成适合ApacheMaven项目的标准目录结构(有关这两个目录介绍的内容,请参看参考资料中的链接)。AppFuse还会从Ant迁移到Maven2上,从而获得相关下载的能力和对生成IDE项目文件的支持。目前基于Ant的系统要求提交者维护项目文件,而Maven2可以通过简单地使用项目的pom.xml文件生成IDEA、Eclipse和NetBeans项目文件。(这个文件位于您项目的根目录中,是使用Maven构建应用程序所需要的主要组件)。它与利用Ant所使用的build.xml文件非常类似。)

现在我们对AppFuse是什么已经有一点概念了,在本文剩下的部分中,我们将介绍使用AppFuse的7点理由。即使您选择不使用AppFuse来开始自己的项目,也会看到AppFuse可以为您提供很多样板代码,这些代码可以在基于Java语言的Web应用程序中使用。由于它是基于Apache许可证的,因此非常欢迎您在自己的应用程序中重用这些代码。

理由1:测试

测试是在软件开发项目中很少被给予足够信任的一个环节。注意我并不是说在软件开发的一些刊物中没有得到足够的信任!很多文章和案例研究都给出了测试优先的开发方式和足够的测试覆盖面以提高软件的质量。然而,测试通常都被看作是一件只会延长项目开发时间的事情。实际上,如果我们使用测试优先的方法在编写代码之前就开始撰写测试用例,我相信我们可以发现这实际上会加速开发速度。另外,测试优先也可以使维护和重用更加容易。如果我们不编写代码来测试自己的代码,那么就需要手工对应用程序进行测试——这通常效率都不高。自动化才是关键。

当我们首次开始使用AppFuse时,我们可能需要阅读这个项目Web站点上提供的快速入门指南和教程(请参看参考资料中的链接)。这些教程的编写就是为了您可以首先编写测试用例;它们直到编写接口和/或实现之后才能编译。如果您有些方面与我一样,就会在开始编写代码之前就已经编写好测试用例了;这是真正可以加速编写代码的惟一方式。如果您首先编写了代码的实现,通过某种方式验证它可以工作,那么您可能会对自己说,“哦,看起来不错——谁需要测试呢?我还有更多的代码需要编写!”这种情况不幸的一面是您通常都会做一些事情来测试自己的代码;您简单地跳过了可以自动化进行测试的地方。

AppFuse的文档展示了如何测试应用程序的所有层次。它从数据库层开始入手,使用了DbUnit(请参看参考资料)在运行测试之前提前使用数据来填充自己的数据库。在数据访问(DAO)层,它使用了Spring的AbstractTransactionalDataSourceSpringContextTests类(是的,这的确是一个类的名字!)来允许简单地加载Spring上下文文件。另外,这个类对每个testXXX()方法封装了一个事务,并当测试方法存在时进行回滚。这种特性使得测试DAO逻辑变得非常简单,并且不会对数据库中的数据造成影响。

在服务层,jMock(请参看参考资料)用来编写那些可以消除DAO依赖的真正单元测试。这允许进行验证业务逻辑正确的快速测试;我们不用担心底层的持久性逻辑。

在Web层,测试会验证操作(Struts/WebWork)、控件(SpringMVC)、页面(Tapestry)和管理bean(JSF)如我们所期望的一样进行工作。Spring的spring-mock.jar可以非常有用地用来测试所有这些框架,因为它包含了一个ServletAPI的仿真实现。如果没有这个有用的库,那么测试AppFuse的Web框架就会变得非常困难。

UI通常是开发Web应用程序过程中最为困难的一部分。它也是顾客最经常抱怨的地方——这既是由于它并不是非常完美,也是由于它的工作方式与我们期望的并不一样。另外,没有什么会比在客户面前作演示的过程中看到看到异常堆栈更糟糕的了!您的应用程序可能会非常可怕,但是客户可能会要求您做到十分完美。永远不要让这种事情发生。CanooWebTest可以对UI进行测试。它使用了HtmlUnit来遍历测试UI,验证所有的元素都存在,并可以填充表单的域,甚至可以验证一个假想的启用Ajax的UI与我们预期的工作方式一样。(有关WebTest和HTMLUnit的链接请参看参考资料。)

为了进一步简化Web的测试,Cargo(请参看参考资料)对Tomcat的启动和停止(分别在运行WebTest测试之前和之后)进行了自动化。

理由2:集成

正如我在本文简介中提到的一样,很多开放源码库都已经预先集成到AppFuse中了。它们可以分为以下几类:

编译、报告和代码生成:Ant、AntContribTasks、Checkstyle、EMMA、Java2Html、PMD和RenamePackages

测试框架:DbUnit、Dumbster、jMock、JUnit和CanooWebTest

数据库驱动程序:MySQL和PostgreSQL

持久性框架:Hibernate和iBATIS

IoC框架:Spring

Web框架:JSF、SpringMVC、Struts、Tapestry和WebWork

Web服务:XFire

Web工具:Clickstream、DisplayTag、DWR、JSTL、SiteMesh、StrutsMenu和URLRewriteFilter

Security:AcegiSecurity

JavaScript和CSS:Scriptaculous、Prototype和MikeStenhouse的CSSFramework

除了这些库之外,AppFuse还使用Log4j来记录日志,使用Velocity来构建e-mail和菜单模板。Tomcat可以支持最新的开发,我们可以使用1.4或5版本的Java平台来编译或构建程序。我们应该可以将AppFuse部署到任何J2EE1.3兼容的应用服务器上;这已经经过了测试,我们知道它在所有主要版本的J2EE服务器和所有主要的servlet容器上都可以很好地工作。

图3给出了上面创建的devworks项目的lib目录。这个目录中的lib.properties文件控制了每个依赖性的版本号,这意味着我们可以简单地通过把这些包的新版本放到这个目录中并执行诸如anttest-all-Dspring.version=2.0之类的命令来测试这些包的新版本。

图3.项目依赖性

预先集成这些开放源码库可以在项目之初极大地提高生产效率。尽管我们可以找到很多文档介绍如何集成这些库,但是定制工作示例并简单地使用它来开发应用程序要更加简单。

除了可以简化Web应用程序的开发之外,AppFuse让我们还可以将Web服务简单地集成到自己的项目中。尽管XFire也在AppFuse下载中一起提供了,不过如果我们希望,也可以自己集成ApacheAxis(请参看参考资料中有关Axis集成的教程)。另外,Spring框架和XFire可以一起将服务层作为Web服务非常简单地呈现出来,这就为我们提供了开发面向服务架构的能力。

另外,AppFuse并不会将我们限定到任何特定的API上。它只是简单地对可用的最佳开放源码解决方案重新进行打包和预先集成。AppFuse中的代码可以处理这种集成,并实现了AppFuse的基本安全性和可用性特性。只要可能,就会减少代码,以便向AppFuse的依赖框架添加一个特性。例如,AppFuse自带的RememberMe和SSL切换特性最近就因为类似的特性而从AcegiSecurity中删除了。

理由3:自动化

Ant使得简化了从编译到构建再到部署的自动化过程。Ant是AppFuse中的一等公民,这主要是因为我发现在命令行中执行操作比从IDE中更加简单。我们可以使用Ant实现编译、测试、部署和执行任何代码生成的任务。

尽管这种能力对于有些人来说非常重要,但是它并不适用于所有的人。很多AppFuse用户目前都使用Eclipse或IntellijIDEA来构建和测试自己的项目。在这些IDE中运行Ant的确可以工作,但是这样做的效率通常都不如使用IDE内置的JUnit支持来运行测试效率高。

幸运的是,AppFuse支持在IDE中运行测试,不过管理这种特性对于AppFuse开发人员来说就变得非常困难了。最大的痛苦在于XDoclet用来生成Hibernate映射文件和Web框架所使用的一些工件(例如ActionForms和Struts使用的struts-config.xml)。IDE并不知道需要生成的代码,除非我们配置使用Ant来编译它们,或者安装了一些可以认识XDoclet的插件。

这种对知识的缺乏是AppFuse2.0切换到JDK5和Maven2上的主要原因。JDK5、注释和Struts2将让我们可以摆脱XDoclet。Maven2将使用这些生成的文件和动态类路径来生成IDE项目文件,这样对项目的管理就可以进行简化。目前基于Ant的编译系统已经为不同的层次生成了一些工件(包括dao.jar、service.jar和webapp.war),因此切换到Maven的模型上应该是一个非常自然的调整。

除了Ant之外(它对于编译、测试、部署和报告具有丰富的支持),对于CruiseControl的支持也构建到了AppFuse中。CruiseControl是一个ContinuousIntegration应用程序,让我们可以在源代码仓库中代码发生变化时自动运行所有的测试。extras/cruisecontrol目录包含了我们为基于AppFuse的项目快速、简单地设置ContinuousIntegration时所需要的文件。

设置ContinuousIntegration是软件开发周期中我们首先要做的事情之一。它不但激发程序员去编写测试用例,而且还通过“Youbrokethebuild!”游戏促进了团队之间的合作和融合。

理由4:安全特性和可扩展性

AppFuse最初是作为我为Apress编写的书籍ProJSP中示例应用程序的一部分开发的。这个示例应用程序展示了很多安全特性和用于简化Struts开发的特性。这个应用程序中的很多安全特性在J2EE的安全框图中都不存在。使用容器管理认证(CMA)的认证方法非常简单,但是RememberMe、密码提示、SSL切换、登记和用户管理等功能却都不存在。另外,基于角色的保护方法功能在非EJB环境中也是不可能的。

最初,AppFuse使用自己的代码和用于CMA的解决方案完全实现了这些特性。我在2004年年初开始学习Spring时就听说过有关AcegiSecurity的知识。我对Acegi所需要的XML的行数(175)与web.xml中所需要的CMA的行数(20)进行了比较,很快就决定丢弃Acegi了,因为它太过复杂了。

一年半之后——在为另外一本书SpringLive中编写了一章有关使用AcegiSecurity的内容之后——我就改变了自己的想法。Acegi的确(目前仍然)需要很多XML,但是一旦我们理解了这一点,它实际上是相当简单的。当我们最终作出改变,使用AcegiSecurity的特性来全部取代AppFuse的特性之后,我们最终删除了大量的代码。类之上的类都已经没有了,“Acegihandlesthatnow”中消失的部分现在全部进入了CVS的Attic中了。

AcegiSecurity是J2EE安全模型中曾经出现过的最好模型。它让我们可以实现很多有用的特性,这些特性在ServletAPI的安全模型中都不存在:认证、授权、角色保护方法、RememberMe、密码加密、SSL切换、用户切换和注销。它让我们还可以将用户证书存储到XML文件、数据库、LDAP或单点登录系统(例如Yale的CentralAuthenticationService(CAS)或者SiteMinder)中。

AppFuse对很多与安全性相关的特性的实现从一开始都是非常优秀的。现在AppFuse使用了AcegiSecurity,这些特性——以及更多特性——都非常容易实现。Acegi有很多地方都可以进行扩充:这是它使用巨大的XML配置文件的原因。正如我们已经通过去年的课程对Acegi进行集成一样,我们已经发现对很多bean的定义进行定制可以更加紧密地与AppFuse建立联系。

SpringIoC容器和AcegiSecurity所提供的简单开发、容易测试的代码和松耦合特性的组合是AppFuse是这么好的一种开发平台的主要原因。这些框架都是不可插入的,允许生成干净的可测试代码。AppFuse集成了很多开放源码项目,依赖注入允许对应用程序层进行简单的集成。

理由5:使用AppGen生成代码

有些人会将代码生成称为代码气味的散播(codesmell)。在他们的观点中,如果我们需要生成代码,那么很可能就会做一些错事。我倾向于这种确定自己代码使用的模式和自动化生成代码的能力应该称为代码香味的弥漫(codeperfume)。如果我们正在编写类似的DAO、管理器、操作或控件,并且不想为它们生成代码,那么这就需要根据代码的气味来生成代码。当然,当语言可以为我们提供可以简化任务的特性时,一切都是那么美好;不过代码生成通常都是一个必需——通常其生产率也非常高——的任务。

AppFuse中提供了一个基于Ant和XDoclet的代码生成工具,名叫AppGen。默认情况下,常见的DAO和管理器都可以允许我们对任何普通老式Java对象(POJO)进行CRUD操作,但是在Web层上这样做有些困难。AppGen有几个特性可以用来执行以下任务:

(使用Middlegen和Hibernate工具)从数据库表中生成POJO

从POJO生成UI

为DAO、管理器、操作/控制器和UI生成测试

在运行AppGen时,您会看到提示说AppGen要从数据库表或POJO中生成代码。如果在命令行中执行antinstall-detailed,AppGen就会安装POJO特定的DAO、管理器以及它们的测试。运行antinstall会导致Web层的类重用通用的DAO和默认存在的管理器。

为了阐述AppGen是如何工作的,我们在devworksMySQL数据库中创建了如清单2所示的表:

清单2.创建一个名为cat的数据库表

createtablecat(

cat_idint(8)auto_increment,

colorvarchar(20)notnull,

namevarchar(20)notnull,

created_datedatetimenotnull,

primarykey(cat_id)

)type=InnoDB;

在extras/appgen目录中,运行antinstall-detailed。这个命令的输出结果对于本文来说实在太长了,不过我们在清单3中给出了第一部分的内容:

清单3.运行AppGen的install-detailed目标

$antinstall-detailed

Buildfile:build.xml

init:

[mkdir]Createddir:/Users/mraible/Work/devworks/extras/appgen/build

[echo]

[echo]+-------------------------------------------------------+

[echo]|--WelcometotheAppGen!--|

[echo]||

[echo]|Usethe"install"targettousethegenericDAOand|

[echo]|Manager,oruse"install-detailed"togeneralaDAO|

[echo]|andManagerspecificallyforyourmodelobject.|

[echo]+-------------------------------------------------------+

[input]WouldyouliketogeneratecodefromatableorPOJO?(table,pojo)

table

[input]Whatisthenameofyourtable(i.e.person)?

cat

[input]Whatisthename,ifany,ofthemoduleforyourtable(i.e.organization)?

[echo]RunningMiddlegentogeneratePOJO...

要对cat表使用这个新生成的代码,我们需要修改src/dao/com/ibm/dao/hibernate/applicationContext-hibernate.xml,来为Hibernate添加Cat.hbm.xml映射文件。清单3给出了我们修改后的sessionFactorybean的样子:

清单4.将Cat.hbm.xml添加到sessionFactorybean中

<beanid="sessionFactory"class="...">

<propertyname="dataSource"ref="dataSource"/>

<propertyname="mappingResources">

<list>

<value>com/ibm/model/Role.hbm.xml</value>

<value>com/ibm/model/User.hbm.xml</value>

<value>com/ibm/model/Cat.hbm.xml</value>

</list>

</property>

...

</bean>

在运行antsetupdeploy之后,我们就应该可以在部署的应用程序中对cat表执行CRUD操作了:

图4.Cat列表

图5.Cat表单

我们在上面的屏幕快照中看到的记录都是作为代码生成的一部分创建的,因此现在就有测试数据了。

理由6:文档

我们可以找到AppFuse各个风味的教程,并且它们都以6种不同的语言给出了:中文、德语、英语、韩语、葡萄牙语和西班牙语。使用风味(flavor)一词,我的意思是不同框架的组合,例如SpringMVC加上iBATIS、SpringMVC加上Hibernate或JSF加上Hibernate。使用这5种Web框架和两种持久框架,可以有好几种组合。有关它们的翻译,AppFuse为自己的默认特性提供了8种翻译。可用语言包括中文、荷兰语、德语、英语、法语、意大利语、葡萄牙语和西班牙语。

除了核心教程之外,还添加了很多教程(请参看参考资料)来介绍与各种数据库、应用服务器和其他开放源码技术(包括JasperReports、Lucene、Eclipse、Drools、Axis和DWR)的集成。

理由7:社区

Apache软件基金会对于开放源码有一个有趣的看法。它对围绕开放源码项目开发一个开放源码社区最感兴趣。它的成员相信如果社区非常强大,那么产生高质量的代码就是一个自然的过程。下面的内容引自Apache主页:

“我们认为自己不仅仅是一组共享服务器的项目,而且是一个开发人员和用户的社区。”

AppFuse社区从2003年作为SourceForge上的一个项目(是struts.sf.net的一部分)启动以来,已经获得了极大的增长。通过在2004年3月转换到java.net上之后,它已经成为这里一个非常流行的项目,从2005年1月到3月成为访问量最多的一个项目。目前它仍然是一个非常流行的项目(有关java.net项目统计信息的链接,请参看参考资料),不过在这个站点上它正在让位于Sun赞助的很多项目。

在2004年年末,NathanAnderson成为继我之后第一个提交者。此后有很多人都加入了进来,包括BenGill、DavidCarter、MikaG?ckel、SanjivJivan和ThomasGaudin。很多现有的提交者都已经通过各种方式作出了自己的贡献,他们都帮助AppFuse社区成为一个迅速变化并且非常有趣的地方。

邮件列表非常友好,我们试图维护这样一条承诺“没有问题是没有人理会的问题”。我们的邮件列表归档文件中惟一一条“RTFM”是从用户那里发出的,而不是从开发者那里发出的。我们绝对信奉Apache开放源码社区的哲学。引用我最好的朋友BruceSnyder的一句话,“我们为代码而来,为人们而留下”。目前,大部分开发者都是用户,我们通常都喜欢有一段美妙的时间。另外,大部分文档都是由社区编写的;因此,这个社区的知识是非常渊博的。

结束语

我们应该尝试使用AppFuse进行开发,这是因为它允许我们简单地进行测试、集成、自动化,并可以安全地生成Web应用程序。其文档非常丰富,社区也非常友好。随着其支撑框架越来越好,AppFuse也将不断改进。

从AppFuse2.0开始,我们计划迁移到JDK5(仍然支持部署到1.4)和Maven2上去。这些工具可以简化使用AppFuse的开发、安装和升级。我们计划充分利用Maven2的功能来处理相关依赖性。我们将碰到诸如appfuse-hibernate-2.0.jar和appfuse-jsf-2.0.jar之类的工件。这些工件都可以在pom.xml文件中进行引用,它们负责提取其他相关依赖性。除了在自己的项目中使用AppFuse基类之外,我们还可以像普通的框架一样在JAR中对这些类简单地进行扩展,这应该会大大简化它的升级过程,并鼓励更多用户将自己希望的改进提交到这个项目中。

如果没有其他问题,使用AppFuse可以让您始终处于JavaWeb开发的技术前沿上——就像我们一样!

相关推荐