Appfuse 开发教程

mud2 贡献于2012-08-25

作者 HUANGNG  创建于2006-06-04 14:01:00   修改者HUANGNG  修改于2006-06-04 23:54:00字数38679

文档摘要:本教程将向你展示如何在一个数据库里创建表,以及如何完成访问这些表的Java代码。我们将建立一个对象以及处理(保存/检索/删除)这些类到数据库的一些代码。用Java术语,我们叫它Plain Old Java Object(POJO)。这个对象通常代表了数据库中的一个表。AppFuse使用Hibernate 作为持久化层, Hibernate是一套对象/关系Object/Relational (O/R)框架,他允许你把Java对象和数据库之间联系起来,它可以很方便的对你的对象执行CRUD (Create, Retrieve, Update, Delete)操作。
关键词:

Appfuse开发教程 目 录 第一部分: 在AppFuse建立DAO和POJO 2 1、建立一个对象,并且作XDoclet标记 2 2、使用Ant根据对象产生数据库表 2 3、建立新的DaoTest来对你的DAO运行JUnit测试 3 4、创建一个对对象执行CRUD操作的新DAO 5 5、配置Spring中的Person和PersonDao 6 6、运行DaoTest 6 第二部分: 创建新的Manager 6 1、创建一个新的运行JUnit测试的ManagerTest 6 2、创建一个新的与DAO通讯的Manager 8 3、为这个Manager和事务配置Spring 8 4、运行ManagerTest 9 第三部分: (Struts) 创建 Struts Actions和JSPs 9 1、为创建generate PersonForm对Person添加XDoclet Tags 9 2、使用XDoclet创建骨架JSPs 9 3、创建测试PersonAction的PersonActionTest 10 4、创建PersonAction 11 5、运行PersonActionTest 13 6、清理JSP来使它更好看 13 7、[可选的] 创建Canoo WebTests来测试模拟浏览器的actions 14 第四部分: (Struts) 增加校验功能和列表页面 15 1、Person.java添加XDoclet的验证标签 15 2、察看和测试添加了验证的JSP 16 3、DAO和Manager的测试添加testGetPeople方法 17 4、PersonDao和Manager添加getPeople方法 17 5、Action添加testSearch方法 18 6、Action添加search方法 18 7、创建personList.jsp和Canoo测试 19 8、菜单添加链接 20 第一部分: 在AppFuse建立DAO和POJO - 讲述如何创建POJO(对应相应的数据表)和 DAO(管理POJO持久化操作的对象)。 本教程将向你展示如何在一个数据库里创建表,以及如何完成访问这些表的Java代码。我们将建立一个对象以及处理(保存/检索/删除)这些类到数据库的一些代码。用Java术语,我们叫它Plain Old Java Object(POJO)。这个对象通常代表了数据库中的一个表,其他的类包括: l 一个数据访问对象Data Access Object (DAO), 一个 Interface和一个Hibernate实现 l 一个 JUnit 类来测试我们的DAO对象 AppFuse使用Hibernate 作为持久化层, Hibernate是一套对象/关系Object/Relational (O/R)框架,他允许你把Java对象和数据库之间联系起来,它可以很方便的对你的对象执行CRUD (Create, Retrieve, Update, Delete)操作。 让我们继续在AppFuse项目的结构下创建一个新的对象、DAO和测试。 1、建立一个对象,并且作XDoclet标记 我们要做的第一件事情就是建立一个需要持久化的对象,我们要在src/dao/**/model目录下建立一个简单的Person对象,这个对象包括id、firstName和lastName属性。 package org.appfuse.model; public class Person extends BaseObject { private Long id; private String firstName; private String lastName; /* * Generate your getters and setters using your favorite IDE:In * Eclipse:Right-click -> Source -> Generate Getters and Setters */ } 这个类必须扩展BaseObject,而这个BaseObject有三个抽象方法(equals(), hashCode()和toString())需要你在Person类里实现,前两个是Hibernate的需要。你可以使用Eclipse来完成。 现在我们已经创建了这个POJO对象,我们需要增加XDoclet标记来产生Hibernate的映射文件,这些文件用来映射对象→ 表和属性(变量) → 字段。 首先,我们增加@hibernate.class 来告诉Hibernate我们将要和那个表作关联: /** * @hibernate.class table="person" */ public class Person extends BaseObject { 我们也要增加主键的映射,否则XDoclet会在产生映射文件时出错,注意所有的@hibernate.*标签必须在getters'的Javadocs里面。 /** * @return Returns the id. * @hibernate.id column="id" generator-class="increment" unsaved-value="null" */ public Long getId() { return this.id; } 我使用generator-class="increment"而不使用generate-class="native" 是因为我对数据库使用"native"时出现问题。建议使用uuid.hex。 2、使用Ant根据对象产生数据库表 在这种情况下,你可以通过运行ant setup-db来建立person表,这个任务会产生文件Person.hbm.xml并且会建立叫做"person"的表,从Ant的控制台窗口,你可以看到Hibernate为你建立的表结构的内容。 [schemaexport] create table person ( [schemaexport] id bigint not null, [schemaexport] primary key (id) [schemaexport] ); 如果你查看Hibernate生成的文件Person.hbm.xml,可以到build/dao/gen/**/model目录,这里是Person.hbm.xml的内容(目前的内容): 现在我们要为其它的字段(first_name, last_name)添加额外的@hibernate.property标签: /** * @hibernate.property column="first_name" length="50" */ public String getFirstName() { return this.firstName; } /** * @hibernate.property column="last_name" length="50" */ public String getLastName() { return this.lastName; } 在这个例子里,添加column属性的唯一原因是因为这个字段名与它的属性名不相同,如果他们相同,你没有必要来指定column属性,关于其它可以使用的标签请看@hibernate.property。 再次运行ant setup-db把新加的属性加到数据库表里。 [schemaexport] create table person ( [schemaexport] id bigint not null, [schemaexport] first_name varchar(50), [schemaexport] last_name varchar(50), [schemaexport] primary key (id) [schemaexport] ); 如果期望修改字段的长度,修改@hibernate.property标签的length属性,如果希望把字段改为必添字段(NOT NULL),可以增加属性not-null="true"。 3、建立新的DaoTest来对你的DAO运行JUnit测试 注意:从Appfuse版本1.6.1+开始包括了一个AppGen工具,可以用来生成本教程余下的所有的类的代码,不过,我们最好还是先过一遍教程再使用这个工具产生代码。 现在,我们要创建一个DaoTest来测试我们的DAO的工作,“等会儿”,你说,“我们还不曾创建DAO呢!”,你说得对。无论如何,我发现先测试后编码大大的促进了软件质量,在许多年里我一直认为在写代码之前写测试是胡说八道,这看起来很愚蠢,但当我尝试之后我认为这样非常好,现在我按照测试驱动的方式工作完全因为我发现这样可以大大提高我软件开发的效率。 开始,我们在test/dao/**/dao目录下建立类PersonDaoTest.java,这个类必须扩展BaseDaoTestCase,而BaseDAOTestCase这个类是JUnit类TestCase的子类,这个类用来加载Spring的ApplicationContext(因为Spring把各个层绑定)和单元测试类同一目录下同你的测试类文件同名的.properties文件(ResourceBundle),这个属性文件的属性可以通过“rb”属性来访问。 我经常拷贝(打开→另存为)一个已存在的测试(如UserDaoTest.java),然后查找/替换 [Uu]ser为[Pp]erson,或者任何其它需要替换的内容。 package org.appfuse.dao; import org.appfuse.model.Person; import org.springframework.dao.DataAccessException; public class PersonDaoTest extends BaseDaoTestCase { private Person person = null; private PersonDao dao = null; public void setPersonDao(PersonDao dao) { this.dao = dao; } } 以上是我们使用JUnit测试而初始化和销毁PersonDao的基本代码,对象“ctx”引用了Spring的ApplicationContext,它在BaseDaoTestCase类的静态代码区里被初始化。 现在我们需要实际测试DAO中的CRUD(create, retrieve, update, delete)方法,为此我们需要为每个方法建立以test(全部小写)开头的测试方法,只要这个方法是公共的,返回类型是void,它们就会被我们build.xml中的Ant的任务调用,如下是一些简单的CRUD测试,需要注意的一点是所有的方法(或者叫做测试)必须是自治的,添加如下代码到文件PersonDaoTest.java: public void testGetPerson() throws Exception { person = new Person(); person.setFirstName("Matt"); person.setLastName("Raible"); dao.savePerson(person); assertNotNull(person.getId()); person = dao.getPerson(person.getId()); assertEquals(person.getFirstName(), "Matt"); } public void testSavePerson() throws Exception { person = dao.getPerson(new Long(1)); person.setFirstName("Matt"); person.setLastName("Last Name Updated"); dao.savePerson(person); if (log.isDebugEnabled()) { log.debug("updated Person: " + person); } assertEquals(person.getLastName(), "Last Name Updated"); } public void testAddAndRemovePerson() throws Exception { person = new Person(); person.setFirstName("Bill"); person.setLastName("Joy"); dao.savePerson(person); assertEquals(person.getFirstName(), "Bill"); assertNotNull(person.getId()); if (log.isDebugEnabled()) { log.debug("removing person..."); } dao.removePerson(person.getId()); try { person = dao.getPerson(person.getId()); fail("Person found in database"); } catch (DataAccessException dae) { log.debug("Expected exception: " + dae.getMessage()); assertNotNull(dae); } } 在testGetPerson方法,我们创建了一个person并且调用get方法,我通常会增加一条我所需要的记录到数据库,因为在测试运行之前DBUnit会为数据库准备测试数据,我们可以简单的在metadata/sql/sample-data.xml里添加测试所必须的记录 idfirst_namelast_name 1 Matt Raible
通过这种方式你可以在testGetPerson方法里消除创建新纪录的动作,如果你愿意直接插入记录到数据库(使用SQL或者GUI),你可以用ant db-export和cp db-export.xml metadata/sql/sample-data.xml重新构建你的sample-data.xml文件。 在上面的例子里,你可以看到我们调用person.set*(value)来准备我们需要保存的对象,在这个例子里很简单,但是当你要插入10条必添字段(not-null="true")时就比较麻烦了,这就是我为什么要在BaseDaoTestCase使用ResourceBundle文件,只要在PersonDaoTest.java同一个目录创建一个PersonDaoTest.properties并且在里面定义你的属性值: 我通常只是在Java里硬编码,但是这个.properties对于大对象很有用。 firstName=Matt lastName=Raible 此时,你要通过调用BaseDaoTestCase.populate(java.lang.Object)方法来准备对象,而不是使用person.set*。 person = new Person(); person = (Person) populate(person); 在目前情况下,还不可以编译PersonDaoTest,因为在类路径里还没有PersonDao.class,我们需要创建它。PersonDao.java是一个接口,PersonDaoHibernate.java是它的Hibernate实现,让我们继续,开始创建。 4、创建一个对对象执行CRUD操作的新DAO 马上,在src/dao/**/dao目录里建立PersonDao.java接口,并且指定所有实现类要实现的基本CRUD操作,为了显示方便,我已经去掉了所有JavaDocs。 package org.appfuse.dao; import org.appfuse.model.Person; public interface PersonDao extends Dao { public Person getPerson(Long personId); public void savePerson(Person person); public void removePerson(Long personId); } 注意,在以上的方法声明上并没有exceptions说明,这是因为Spring使用RuntimeExceptions来包裹Exceptions的方式,此时,你已经可以使用ant compile-dao来编译src/dao和test/dao下的所有源文件,然而当你运行ant test-dao -Dtestcase=PersonDao进行测试时,你会得到一个错误:No bean named 'personDao' is defined,这是一个Spring的错误,说明你必须在applicationContext-hibernate.xml指定一个名字为personDAO的bean,在此之前我们需要创建PersonDao的实现类。 运行dao测试的ant任务叫做test-dao,如果你传递testcase参数(用-Dtestcase=name),它会查看**/*${testcase}*允许我们传递Person、PersonDao、或者PersonDaoTest以及所有会执行PersonDaoTest的类。 让我们创建一个实现PersonDao的类PersonDaoHibernate并使用Hibernate来get/save/delete这个Person对象,为此,我们在src/dao/**/dao/hibernate创建一个新类PersonDaoHibernate.java,它应该扩展BaseDaoHibernate,并且实现PersonDao。为了简洁,省略Javadocs。 package org.appfuse.dao.hibernate; import org.appfuse.model.Person; import org.appfuse.dao.PersonDao; import org.springframework.orm.ObjectRetrievalFailureException; public class PersonDaoHibernate extends BaseDaoHibernate implements PersonDao { public Person getPerson(Long id) { Person person = (Person) getHibernateTemplate().get(Person.class, id); if (person == null) { throw new ObjectRetrievalFailureException(Person.class, id); } return person; } public void savePerson(Person person) { getHibernateTemplate().saveOrUpdate(person); } public void removePerson(Long id) { // object must be loaded before it can be deleted getHibernateTemplate().delete(getPerson(id)); } } 现在,如果你运行ant test-dao -Dtestcase=PersonDao,你会得到同样的错误,我们必须配置Spring来让它知道PersonDaoHibernate是PersonDao的实现,同样的,我们也要告诉它还有个Person对象。 5、配置Spring中的Person和PersonDao 首先我们要告诉Spring所有Hibernate文件的位置,为此,打开src/dao/**/dao/hibernate/applicationContext-hibernate.xml,在以下代码块添加"Person.hbm.xml"。 org/appfuse/model/Person.hbm.xml org/appfuse/model/Role.hbm.xml org/appfuse/model/User.hbm.xml 现在我们需要添加一些XML数据来绑定PersonDaoHibernate到PersonDao,为此,添加如下代码到文件底部: 你也可以为使用autowire="byName"属性来消除"sessionFactory"属性。从个人来讲,我喜欢在XML文件里保留对象的依赖。 6、运行DaoTest 保存所有修改的文件,运行ant test-dao -Dtestcase=PersonDao。 Yeah Baby, Yeah: BUILD SUCCESSFUL Total time: 9 seconds 第二部分: 创建新的Manager - 讲述如何创建一个 Business Facades ,它可以跟数据层 (DAOs)和web 层 (Actions 或 Controllers)交互。 本教程将会向你展示如何创建一个业务Facade类(和一个JUnit Test)与Part I建立DAO交互操作。 在AppFuse的语境下,这被称作一个Manager类,它的主要职责是持久户层(DAO)和web层之间的一个桥梁,它也很好的把展示层和数据库层(例如Swing应用)解耦,Managers必定是应用程序所有的业务逻辑所在的地方。 让我们从在AppFuse的框架下创建一个ManagerTest和Manager。 1、创建一个新的运行JUnit测试的ManagerTest 在Part I,我们创建了一个Person对象和一个PersonDao对象 - 所以我们继续开发这个实体,首先,我们创建PersonManager的JUnit test,在test/service/**/service目录下创建PersonManagerTest,我们会希望在DAO对象同样的基本方法(get, save, remove) 测试。 这看起来是多余的(为什么全是测试!),但如果是一个6个月的过程这个测试是非常重要的。 这个类必须扩展service包下的BaseManagerTestCase,这个类(BaseManagerTestCase)的功能与BaseDaoTestCase类似。 我通常会修改(打开 → 另存为)存在的测试(如UserManagerTest.java),查找/替换[Uu]ser with [Pp]erson,或者其他任何我的对象的名字。 以下代码是一个基本的Manager的JUnit测试的要求,与DaoTest不同,这个测试使用jMock来吧Manager和他的依赖隔离,使它成为一个真的"单元" 测试。这可以使你只关心业务逻辑而不必担心它的依赖,以下代码简单的设置好Manager和它的依赖。 package org.appfuse.service; import java.util.List; import java.util.ArrayList; import org.appfuse.dao.PersonDao; import org.appfuse.model.Person; import org.appfuse.service.impl.PersonManagerImpl; import org.jmock.Mock; import org.springframework.orm.ObjectRetrievalFailureException; public class PersonManagerTest extends BaseManagerTestCase { private final String personId = "1"; private PersonManager personManager = new PersonManagerImpl(); private Mock personDao = null; private Person person = null; protected void setUp() throws Exception { super.setUp(); personDao = new Mock(PersonDao.class); personManager.setPersonDao((PersonDao) personDao.proxy()); } protected void tearDown() throws Exception { super.tearDown(); personManager = null; } } 现在你已经把类的骨架搭好了,你需要添加肉了:填写确保所有测试通过的代码,以下来自DAO Tutorial的片断帮助我们理解我们将要做的事情。 ...我们创建以"test"(全部小写)开头的方法,只要这些方法是public,返回类型是void,并且没有参数,它们就会被调用,以下是为了测试简单的CRUD操作,一件需要记住的事情是每一个方法(也可以称作测试)必须是自治的。 添加如下方法到PersonManagerTest.java: public void testGetPerson() throws Exception { // set expected behavior on dao personDao.expects(once()).method("getPerson") .will(returnValue(new Person())); person = personManager.getPerson(personId); assertTrue(person != null); personDao.verify(); } public void testSavePerson() throws Exception { // set expected behavior on dao personDao.expects(once()).method("savePerson") .with(same(person)).isVoid(); personManager.savePerson(person); personDao.verify(); } public void testAddAndRemovePerson() throws Exception { person = new Person(); // set required fields person.setFirstName("firstName"); person.setLastName("lastName"); // set expected behavior on dao personDao.expects(once()).method("savePerson") .with(same(person)).isVoid(); personManager.savePerson(person); personDao.verify(); // reset expectations personDao.reset(); personDao.expects(once()).method("removePerson").with(eq(new Long(personId))); personManager.removePerson(personId); personDao.verify(); // reset expectations personDao.reset(); // remove Exception ex = new ObjectRetrievalFailureException(Person.class, person.getId()); personDao.expects(once()).method("removePerson").isVoid(); personDao.expects(once()).method("getPerson").will(throwException(ex)); personManager.removePerson(personId); try { personManager.getPerson(personId); fail("Person with identifier '" + personId + "' found in database"); } catch (ObjectRetrievalFailureException e) { assertNotNull(e.getMessage()); } personDao.verify(); } 这个类不会被编译,因为我们还没有创建PersonManager接口。 在AppFuse里遵从这么多规范来实现可扩展性看起来很可笑,事实上,在绝大多数我参与的项目里 - 我发现在一年里学了如此多的知识,以至于我不想扩展我的架构,我想去重写它,我希望通过采纳最佳实践来保持AppFuse的时效性,但这并不经常发生,每年都仅仅是一个道最新版本的升级,而不是一个重写,;-) 2、创建一个新的与DAO通讯的Manager 马上,为所有实现类在src/service/**/service目录创建一个PersonManager.java接口来指定基本的CRUD操作,为了显示的目的,我去掉了所有的JavaDocs, setPersonDao()方法不是在所有的情况下出现,只是因为PersonManagerTest可以把DAO赋值。 通常,我会复制(打开 → 另存为)一个已存在的文件 (例如UserManager.java). package org.appfuse.service; import org.appfuse.model.Person; import org.appfuse.dao.PersonDao; public interface PersonManager { public void setPersonDao(PersonDao dao); public Person getPerson(String id); public void savePerson(Person person); public void removePerson(String id); } 我们创建一个PersonManagerImpl类来实现PersonManager中的方法,为此,在src/service/**/service/impl创建一个PersonManagerImpl.java类,他必须扩展BaseManage并且实现PersonManager。 package org.appfuse.service.impl; import org.appfuse.model.Person; import org.appfuse.dao.PersonDao; import org.appfuse.service.PersonManager; public class PersonManagerImpl extends BaseManager implements PersonManager { private PersonDao dao; public void setPersonDao(PersonDao dao) { this.dao = dao; } public Person getPerson(String id) { return dao.getPerson(Long.valueOf(id)); } public void savePerson(Person person) { dao.savePerson(person); } public void removePerson(String id) { dao.removePerson(Long.valueOf(id)); } } 需要注意的是setPersonDao()方法,Spring使用它来绑定PersonDao到Manager,这些配置在applicationContext-service.xml 文件,我们将在Step 3[3]配置这些,现在你可以使用"ant compile-service"编译所有代码。 现在你需要为服务层配置Spring文件,它才会知道这个新的Manager。 3、为这个Manager和事务配置Spring 为了通知Spring我们的PersonManager接口和它的实现类,打开src/service/**/service/applicationContext-service.xml,你会看到注释掉的关于"personManager"的定义,去掉注释,或者直接在文件末尾添加: "parent"属性会引用一个TransactionProxyFactoryBeanbean的定义,这也是所有的事物对象所要设置的。 4、运行ManagerTest 保存所有的文件,并且运行ant test-service -Dtestcase=PersonManager。 Yeah Baby, Yeah: BUILD SUCCESSFUL Total time: 9 seconds 第三部分: (Struts) 创建 Struts Actions和JSPs - 讲述如何创建在你自己的appfuse工程里面创建Actions和JSPs。包括生成JSP并且进行修改定制让它们好看一点。此外,你需要编写WebTest来测试这个JSP的功能。 本教程将向你展示如何创建一个Struts Action,一个JUnit Test(使用 StrutsTestCase),和一个form的JSP,我们创建的这个Action将与教程“创建新的Manager”创建的PersonManager交互。 通常情况下,AppFuse使用Struts作为它的web框架,作为1.6+,你可以使用Spring或者WebWork作为web框架,在1.7, 增加了对JSF和Tapestry的支持。 如果希望安装struts以外的web框架,只需转到extras目录下你所期望安装的框架目录下,在相应目录下的README.txt文件会有进一步的说明,针对其他几个框架的教程列在下面。 l Spring: Creating Spring Controllers and JSPs l WebWork: Creating WebWork Actions and JSPs l JSF: Creating JSF Beans and JSPs l Tapestry: Creating Tapestry Pages and Templates 让我们从创建一个新的Struts Action和JSP作为开始。 1、为创建generate PersonForm对Person添加XDoclet Tags 我们首先在web层为Struts创建PersonForm对象,为此,我们需要对Person.java添加标签来产生我们的Struts ActionForm,在Person.java的JavaDoc添加@struts.form标签(如果你需要一个实例,可以参考User.java): * @struts.form include-all="true" extends="BaseForm" 我们扩展org.appfuse.webapp.form.BaseForm,因为它的方法toString()中会调用log.debug(formName)来打印易读的Form信息。 如果你没有把"org.appfuse"包换成你的"com.company"或者没有把你的模型类放到默认的包,你必须确认在@struts.form中的标签对org.appfuse.webapp.form.BaseForm使用完全的引用。 Now if you run ant gen-forms, Ant (and XDoclet) will generate a PersonForm.java for you in build/web/gen/**/form. 2、使用XDoclet创建骨架JSPs 在这一步,你将会创建来显示Person对象信息的JSP页面,它会包括Strut的JSP标签用来表现Person.java每一个属性,AppGen工具建立在一个StrutsGen工具上,用来做这件事,这个StrutsGen工具起初由Erik Hatcher编写,它只是一些XDoclet模版和一些附加类,所有的这些文件在extras/appgen目录里。 以下是生成JSP和包含标签及表单元素属性文件的简单步骤 l 在命令行环境下,转到"extras/appgen"目录 l 执行ant -Dobject.name=Person -Dappgen.type=pojo会在extras/appgen/build/gen产生一些文件,事实上,它会产生本教程所需的所有文件,但是我们只获取我们所需要的那些。 n web/WEB-INF/classes/Person.properties (form元素的标签) n web/pages/personForm.jsp (察看单个文件的JSP文件) n web/pages/personList.jsp (察看People列表的JSP文件) l 把Person.properties的内容拷贝到web/WEB-INF/classes/ApplicationResources.properties,这是所有form中需要的标题关键字,以下是你将要添加到ApplicationResources.properties的内容的例子: # -- person form -- personForm.id=Id personForm.firstName=First Name personForm.lastName=Last Name person.added=Person has been added successfully. person.updated=Person has been updated successfully. person.deleted=Person has been deleted successfully. # -- person list page -- personList.title=Person List personList.heading=Persons # -- person detail page -- personDetail.title=Person Detail personDetail.heading=Person Information 拷贝personForm.jsp到web/pages/personForm.jsp,拷贝personList.jsp到web/pages/personList.jsp,注意,所有新建文件的第一个字母是小写的。 "pages"目录中的文件在部署环境下会放到"WEB-INF/pages"目录下,容器会对所有WEB-INF目录下的文件提供安全保护,这种保护针对客户端的请求,而通过Struts ActionServlet的转发可以访问,把这些JSPs文件放到WEB-INF目录下确保它们只会被Actions访问,而不会被客户或者彼此直接访问,这允许安全保护转移到Action,这样可以保证保护更有效,脱离表示层这个基础。 AppFuse的web应用安全机制确认所有的*.html形式的访问是被保护的(除了/signup.html和/passwordHint.html),这保证了客户必须通过Action来访问JSP(或者至少pages中的一些)。 注意:如果你想为某一页定制CSS,你必须在文件的开头添加,这会被SiteMesh看到并且应用到最终的页面,然后你可以一页一页的定制你的CSS,就像如下的方式: body#pageName element.class { background-color: blue } 在ApplicationResources.properties中添加JSP文件出现的标题和题头,在生成的JSPs里有两处标题(浏览器顶端显示)的关键字,这些字段提供了personDetail.title和personDetail.heading关键字名。 如上,我们在文件里添加"personForm.*"关键字,我们为什么使用personDetail而不是personForm作为标题呢?最好的一个原因是我们需要区分form的标签和页面上的文本,另一个原因是*Form.*的形式给你数据库里所有的字段更好的展现形式。 最近我有一个客户期望数据库里的所有字段是可查询的,这样作就会比较容易,我只需要查看ApplicationResources.properties中的保存的所有关键字来寻找"Form.",然后记录下来,在用户界面下,用户只需要输入想查找的项目和字段。我对能够在项目里区分对待Form和Detail感到高兴! 3、创建测试PersonAction的PersonActionTest 为了给PersonAction创建StrutsTestCase Test,首先要在test/web/**/action目录下创建PersonActionTest.java文件: package org.appfuse.webapp.action; import org.appfuse.Constants; import org.appfuse.webapp.form.PersonForm; public class PersonActionTest extends BaseStrutsTestCase { public PersonActionTest(String name) { super(name); } public void testEdit() throws Exception { setRequestPathInfo("/editPerson"); addRequestParameter("method", "Edit"); addRequestParameter("id", "1"); actionPerform(); verifyForward("edit"); assertTrue(request.getAttribute(Constants.PERSON_KEY) != null); verifyNoActionErrors(); } public void testSave() throws Exception { setRequestPathInfo("/editPerson"); addRequestParameter("method", "Edit"); addRequestParameter("id", "1"); actionPerform(); PersonForm personForm = (PersonForm) request.getAttribute(Constants.PERSON_KEY); assertTrue(personForm != null); setRequestPathInfo("/savePerson"); addRequestParameter("method", "Save"); // update the form from the edit and add it back to the request personForm.setLastName("Feltz"); request.setAttribute(Constants.PERSON_KEY, personForm); actionPerform(); verifyForward("edit"); verifyNoActionErrors(); } public void testRemove() throws Exception { setRequestPathInfo("/editPerson"); addRequestParameter("method", "Delete"); addRequestParameter("id", "2"); actionPerform(); verifyForward("mainMenu"); verifyNoActionErrors(); } } 你需要为src/dao/**/Constants.java添加PERSON_KEY变量,这个"personForm"与struts-config.xml中的名称相对应。 /** * The request scope attribute that holds the person form. */ public static final String PERSON_KEY = "personForm"; 如果你尝试运行测试,你会得到几个NoSuchMethodErrors,所以,让我们定义PersonAction的edit、save和delete方法。 4、创建PersonAction 在src/web/**/action,创建PersonAction.java,有下面的内容: package org.appfuse.webapp.action; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; import org.apache.struts.action.ActionMessage; import org.apache.struts.action.ActionMessages; import org.appfuse.model.Person; import org.appfuse.service.PersonManager; import org.appfuse.webapp.form.PersonForm; /** * @struts.action name="personForm" path="/editPerson" scope="request" * validate="false" parameter="method" input="mainMenu" */ public final class PersonAction extends BaseAction { public ActionForward cancel(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { return mapping.findForward("mainMenu"); } public ActionForward delete(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { if (log.isDebugEnabled()) { log.debug("Entering 'delete' method"); } ActionMessages messages = new ActionMessages(); PersonForm personForm = (PersonForm) form; // Exceptions are caught by ActionExceptionHandler PersonManager mgr = (PersonManager) getBean("personManager"); mgr.removePerson(personForm.getId()); messages.add(ActionMessages.GLOBAL_MESSAGE, new ActionMessage("person.deleted")); // save messages in session, so they'll survive the redirect saveMessages(request.getSession(), messages); return mapping.findForward("mainMenu"); } public ActionForward edit(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { if (log.isDebugEnabled()) { log.debug("Entering 'edit' method"); } PersonForm personForm = (PersonForm) form; // if an id is passed in, look up the user - otherwise // don't do anything - user is doing an add if (personForm.getId() != null) { PersonManager mgr = (PersonManager) getBean("personManager"); Person person = mgr.getPerson(personForm.getId()); personForm = (PersonForm) convert(person); updateFormBean(mapping, request, personForm); } return mapping.findForward("edit"); } public ActionForward save(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { if (log.isDebugEnabled()) { log.debug("Entering 'save' method"); } // Extract attributes and parameters we will need ActionMessages messages = new ActionMessages(); PersonForm personForm = (PersonForm) form; boolean isNew = ("".equals(personForm.getId())); if (log.isDebugEnabled()) { log.debug("saving person: " + personForm); } PersonManager mgr = (PersonManager) getBean("personManager"); Person person = (Person) convert(personForm); mgr.savePerson(person); // add success messages if (isNew) { messages.add(ActionMessages.GLOBAL_MESSAGE, new ActionMessage("person.added")); // save messages in session to survive a redirect saveMessages(request.getSession(), messages); return mapping.findForward("mainMenu"); } else { messages.add(ActionMessages.GLOBAL_MESSAGE, new ActionMessage("person.updated")); saveMessages(request, messages); return mapping.findForward("edit"); } } } 你会注意到以上的代码有许多对convert的调用来转化PersonForm到Person,这个转化convert方法在BaseAction.java(调用ConvertUtil.convert())中,并且使用BeanUtils.copyProperties 来实现POJOs → ActionForms和ActionForms → POJOs的转化。 如果你运行Eclipse,你需要对project运行"refresh"来显示PersonForm,它在build/web/gen目录中,这个目录必须是project的源目录之一,这是Eclipse看到和导入PersonForm的唯一方式,因为它是通过XDoclet产生并且不在通常的源目录中,你可以在build/web/gen/org/appfuse/webapp/form/PersonForm.java找到它。 在BaseAction里你可以定义自己的转换器(例如 DateConverter),因此BeanUtils.copyProperties能够知道Strings → Objects的转化。如果你的POJOs存在Lists(例如 父子关系),你需要使用convertLists(java.lang.Object)方法手工完成这个转化。 现在你需要添加edit forward和savePerson action-mapping,这两个早就在PersonActionTest中提到,为此,可以在PersonAction.java添加一些额外的XDoclet标签,在类的声明前面加入这些注释,你一定已经有了editPerson action-mapping的XDoclet标签,我在这里展示你会在类中看到的所有的XDoclet标签。 /** * @struts.action name="personForm" path="/editPerson" scope="request" * validate="false" parameter="method" input="mainMenu" * * @struts.action name="personForm" path="/savePerson" scope="request" * validate="true" parameter="method" input="edit" * * @struts.action-forward name="edit" path="/WEB-INF/pages/personForm.jsp" */ public final class PersonAction extends BaseAction { action-mappings中editPerson和savePerson主要区别是savePerson的XDoclet标签的validation标记是true(见validation="true"),注意"input"属性必须指向到一个forward,而不是个路径(例如/editPerson.html),如果你希望edit同save一样是是用用save的路径也是可以的,只要确保validate="false",然后在你的"save"方法中,你可以执行form.validate()来处理错误。你一定注意到了这里是用PersonManager的方式与PersonManagerTest中的一样,PersonAction和PersonManagerTest都是PersonManagerImpl的客户,这很有意义。 本教程几乎所有的事情都搞定了,让我们来运行这些测试! 5、运行PersonActionTest 如果你查看PersonActionTest,你会发现所有的测试依赖于数据库中一条id=1的记录(testRemove依赖于id=2),所以要添加那些样本数据(metadata/sql/sample-data.xml),我会添加到底部,顺序并不重要(目前),因为它们互相并不存在依赖关系。 idfirst_namelast_name 1 Matt Raible 2 James Davidson
DBUnit会在执行测试前加载这些数据,所以这些记录PersonActionTest可以得到。 如果现在你执行ant test-web -Dtestcase=PersonAction - 所有事情会按计划执行,在作尝试前请确定Tomcat已经运行。 BUILD SUCCESSFUL Total time: 1 minute 21 seconds 6、清理JSP来使它更好看 让我们开始清理personForm.jsp,修改的action为"savePerson",这保证验证起作用,同时修改focus属性,从focus=""改为focus="firstName",这样在页面打开后光标会落到firstName字段(使用JavaScript)。 还有一件得做的事是删除(注解)掉personForm.jsp文件底的下述几行代码,这是因为如果定义了formName 而校验规则不存在时,Validator会抛出异常。 我个人认为这是一个bug,而Struts委员会的人不这么认为。 现在你如果执行ant db-load deploy,启动Tomcat,然后在你的浏览器输入http://localhost:8080/appfuse/editPerson.html?id=1,你会看到如下信息: 注意:如果你修改了web中的任何文件,请使用deploy-web,否则,使用deploy来编译和部署。 最后,为了使页面更加的用户友好,你也许希望为用户在form上面添加一段信息,只需在personForm.jsp页的顶端添加(使用 )。 7、[可选的] 创建Canoo WebTests来测试模拟浏览器的actions 本教程最后的步骤(可选的)是创建Canoo WebTest来测试JSPs。 我说是可选的,是因为你可以通过浏览器运行同样的测试。 你可以使用如下的URLs来测试不同actions的adding、editing和saving。 l Add - http://localhost:8080/appfuse/editPerson.html. l Edit - http://localhost:8080/appfuse/editPerson.html?id=1 (make sure and run ant db-load first). l Delete - http://localhost:8080/appfuse/editPerson.html?method=Delete&id=1 (or edit and click on the Delete button). l Save - Click edit and then click the Save button. Canoo测试非常的灵活,只需要配置XML文件,为了测试add、edit、save和delete,打开test/web/web-tests.xml添加如下的XML,你一定注意到这些片断的target名称是PersonTests,会运行所有相关的测试。 我使用CamelCase的target命名方式(vs. 传统的小写,下划线分割的方式)是因为当拼写-Dtestcase=Name时,我会经常习惯于使用CamelCase方式来命名我的JUnit Tests。 Successfully ran all Person JSP tests! &config; &login; &config; &login; &config; &login; &config; &login; 完成添加后,你一定可以在Tomcat运行时运行ant test-canoo -Dtestcase=PersonTests,或者使用ant test-jsp -Dtestcase=PersonTests让Ant为你启动/关闭Tomcat,为了在所有的Canoo测试中包括PersonTests,把它作为"run-all-tests"的依赖target。 你必须注意到Canoo没有客户端的日志,如果你期望看到一些信息,你可以在每一个target的和之间添加如下代码。 ${web-tests.result} BUILD SUCCESSFUL Total time: 11 seconds 第四部分: (Struts) 增加校验功能和列表页面 - 增加一个验证personForm的firstName和lastName为必填项的校验逻辑,并且增加一个列表面显示数据库中所有的person记录。 本教程将向你展示如何用Struts的Validator为PersonForm添加验证逻辑(客户端和服务器端),我们也要使用Display Tag Library创建一个列表页来显示数据库中所有的人。 1、Person.java添加XDoclet的验证标签 为了使用Struts的Validator,通常情况下你需要手工写validation.xml,如果你不使用AppFuse,你还需要配置Validator插件和在ApplicationResources.properties写错误关键字,可以到Validation Made Easy Tutorial(Struts本身也有一套rich set of tutorials)察看更多的信息。 由于XDoclet,事情简单多了 - 只需要给Person类添加一些@struts.validator标签,打开文件(src/dao/**/model/Person.java)修改getFirstName()和getLastName()方法使之包括@struts.validator type="required"标签。 /** * @struts.validator type="required" * @hibernate.property column="first_name" length="50" */ public String getFirstName() { return this.firstName; } /** * @struts.validator type="required" * @hibernate.property column="last_name" length="50" */ public String getLastName() { return this.lastName; } 你也可以修改msgkey属性来覆盖这个错误的默认属性。 @struts.validator type="required" msgkey="errors.required" type="required"的属性的默认关键字是errors.required,所以我通常会保留默认值。这个默认值定义在web/WEB-INF/classes/ApplicationResources_*.properties,你一定会注意到我们把标签放到getters方法前,而不是XDoclet 文档里说的放到setters前,这是因为我们生成PersonForm.java的模版文件(metadata/template/struts_form.xdt)会把标签加到生成的文件里。 如果你现在保存Person.java并且运行ant clean webdoclet,就会在build/appfuse/WEB-INF/生成一个validation.xml文件,它的内容一定保存了一条关于"personForm"的条目。
为了给personForm.jsp开启客户端的验证,一个javascript的JSP标签和一段脚本需要添加到personForm.jsp的底端,它们一定已经存在了(幸亏了appgen)- 你只需要去掉注释符号。我们去掉注释是因为,如果我们指定了formName而没有对应的验证规则存在,就会有exception抛出。 从个人角度讲,我认为这是一个bug,但是Struts开发者不这么认为。 注意: 如果你绑定验证规则到对象,它将会被收集起来添加到validation.xml,这是因为当一个Form建立的时候,一个@struts.validator会添加到关联对象的Setter上(通过metadata/templates/struts_form.xdt)。如果你使用多对多的双向关联,这会引起一个问题,有两个解决方案,第一个是在struts_form.xdt中去掉@struts.validator标签,并且手动的修改POJO的setter,第二种方案在这里描述。 2、察看和测试添加了验证的JSP 你现在有了这个form的验证配置,无论何时这个form在被调用时,如果action-mapping中的validate="true",这些规则就会被应用,在上一个教程里,我们为PersonAction添加了"savePerson"的action-mapping,对应的XDoclet标签如下: * @struts.action name="personForm" path="/savePerson" scope="request" * validate="true" parameter="method" input="edit" 所以现在只要你的web/pages/personForm.jsp包括了,当你保存这个form时就会调用验证,运行ant db-load deploy,启动Tomcat,浏览到http://localhost:8080/appfuse/editPerson.html?id=1。 如果你清除firstName和lastName的值,你就会得到如下的JavaScript错误警告。 为了保证事情确实按照所想的工作,你可以关闭JavaScript来确定服务器端的验证正在工作,这对Mozilla Firebird(我最喜欢的浏览器)来说非常简单,只需要到Tools → Options → Web Features去掉"Enable JavaScript",现在如果你清除字段的值并且保存form,你会看到如下信息: 如果你没有见到这些验证错误,有以下几个可能: l 保存form返回成功信息,但是firstName和lastName字段是空白的。 n 这是因为web/pages/personForm.jsp的有action="editPerson" - 确定存在action="savePerson"。 l 点击保存,显示一个空白页。 n 空白页标明你的"savePerson" forward的"input"属性配置不正确,确定它是和一个local或者global的action-forward关联,在这个例子里,它一定是input="edit",指向到.personDetail的tiles定义,根据我的经验,input的值应该是一个forward,而不应该是到一个action的路径。 如果你只希望服务器端的验证(没有JavaScript),你可以删除(在web/pages/personForm.jsp)的onsubmit属性,还有就是此页底端的JavaScript验证标签。 3、DAO和Manager的测试添加testGetPeople方法 为了创建一个List页面(也被称作master screen),我们需要创建返回person表所有行的方法,首先我们要给PersonDaoTest和PersonManagerTest添加这些测试方法,我通常会命名为getEntities(例如getUsers),但是你也可以使用getAll或者search - 这仅仅是一个个人爱好。 打开test/dao/**/dao/PersonDaoTest.java添加testGetPeople方法: public void testGetPeople() { person = new Person(); List results = dao.getPeople(person); assertTrue(results.size() > 0); } 我传递给getPeople方法的person对象是为了以后用来过滤(根据person对象中的值),在你的getPeople()方法添加参数是可选的,但余下的教程假定你是这样做的。 打开test/service/**/service/PersonManagerTest.java添加testGetPeople方法: public void testGetPeople() throws Exception { List results = new ArrayList(); person = new Person(); results.add(person); // set expected behavior on dao personDao.expects(once()).method("getPeople") .will(returnValue(results)); List people = personManager.getPeople(null); assertTrue(people.size() == 1); personDao.verify(); } 为了编译通过,你必须给PersonDao和PersonManager添加getPeople()方法和方法的实现。 4、PersonDao和Manager添加getPeople方法 打开src/dao/**/dao/PersonDao.java添加getPeople()方法的签名: public List getPeople(Person person); 添加同样的方法签名到src/service/**/service/PersonManager.java,保存所有的文件并且调整测试的imports,然后需要在实现类里实现getPeople()方法,打开src/dao/**/dao/hibernate/PersonDaoHibernate.java添加如下代码: public List getPeople(Person person) { return getHibernateTemplate().find("from Person"); } 你会注意到我们对参数person未作任何操作,这里它只是一个占位符 - 将来你会通过它的属性用Hibernate's Query Language (HQL)或者Criteria Queries来过滤查询。 一个Criteria Query的例子: // filter on properties set in the person object HibernateCallback callback = new HibernateCallback() { public Object doInHibernate(Session session) throws HibernateException { Example ex = Example.create(person).ignoreCase().enableLike(MatchMode.ANYWHERE); return session.createCriteria(Person.class).add(ex).list(); } }; return (List) getHibernateTemplate().execute(callback); 现在实现src/service/**/impl/PersonManagerImpl.java里的getPeople()方法: public List getPeople(Person person) { return dao.getPeople(person); } 保存以上的修改,你应当可以运行如下的测试: l ant test-dao -Dtestcase=PersonDao l ant test-service -Dtestcase=PersonManager 如果所有的事工作正常- nice job!,现在需要在web层添加retrieve all功能。 5、Action添加testSearch方法 打开test/web/**/action/PersonActionTest.java并且添加如下代码: public void testSearch() { setRequestPathInfo("/editPerson"); addRequestParameter("method", "Search"); actionPerform(); verifyForward("list"); assertNotNull(getRequest().getAttribute(Constants.PERSON_LIST)); verifyNoActionErrors(); } 这个类在对src/dao/**/Constants.java添加PERSON_LIST变量PERSON_LIST之前不可以编译。 我通常会拷贝一个已经存在的类似变量 - 如USER_LIST。 /** * The request scope attribute that holds the person list */ public static final String PERSON_LIST = "personList"; 现在保存所有的修改,你还不可以运行ant test-web -Dtestcase=PersonAction,因为PersonAction.search()还不存在。 6、Action添加search方法 打开src/web/**/action/PersonAction.java然后在我们列表页面的顶端添加如下XDoclet标签。 * @struts.action-forward name="list" path="/WEB-INF/pages/personList.jsp" 现在给PersonAction类添加查询方法。 我通常会用UserAction.search()作为此方法的模版。 public ActionForward search(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { if (log.isDebugEnabled()) { log.debug("Entering 'search' method"); } PersonManager mgr = (PersonManager) getBean("personManager"); List people = mgr.getPeople(null); request.setAttribute(Constants.PERSON_LIST, people); // return a forward to the person list definition return mapping.findForward("list"); } 运行ant test-web -Dtestcase=PersonAction. Nice! BUILD SUCCESSFUL Total time: 1 minute 26 seconds 7、创建personList.jsp和Canoo测试 打开web/pages中的personList.jsp文件,文件的顶端是一个标签,把编辑界面的forward暴露为page-scoped的一个变量,这个的值现在一定是"editPerson"。 <%-- For linking to edit screen --%> 添加这些到metadata/web/global-forwards.xml,类似于list中的那些代码,如此,它们就会被包含在struts-config.xml文件中。 另一件你希望修改的事是改变列表中的项目为复数形式,例子里生成的名字是"persons",它应该为people,在31行或者附近,你会看到如下的代码: 修改为: 最后,web/WEB-INF/classes/ApplicationResources.properties里添加标题和题目关键字(personList.title和personList.heading),打开文件添加如下内容: # -- person list page -- personList.title=Person List personList.heading=All People 提醒一下,personList.title将会出现在浏览器的标题栏(标签),而personList.heading会作为<h1>标签在页面正文内容前面。 此刻,你一定可以运行ant clean deploy,启动Tomcat,在浏览器里查看http://localhost:8080/appfuse/editPerson.html?method=Search。 现在你有了个列表页面,让我们修改添加和删除一个Person后转向的页面,在src/web/**/action/PersonAction.java修改方法save、 delete和cancel的mapping.findForward("mainMenu")为如下内容: return mapping.findForward("viewPeople"); 你也需要修改test/web/**/action/PersonActionTest.java中testRemove方法的verifyForward("mainMenu")为verifyForward("viewPeople"),最后,需要修改Canoo测试的"AddPerson"和"DeletePerson",打开test/web/web-tests.xml并且修改"AddPerson"目标为如下代码: <verifytitle stepid="Main Menu appears if save successful" text=".*${mainMenu.title}.*" regex="true"/> 改为: <verifytitle stepid="Person List appears if save successful" text=".*${personList.title}.*" regex="true"/> 然后对于"DeletePerson",修改如下: <verifytitle stepid="display Main Menu" text=".*$(mainMenu.title).*" regex="true"/> 改为: <verifytitle stepid="display Person List" text=".*${personList.title}.*" regex="true"/> 我们使用"viewPeople"而不是"list",是因为查询方法是要被执行的,而不是简单的转向到personForm.jsp。 为了测试这个页面已经工作了,在test/web/web-tests.xml创建一个新的JSP测试: <!-- Verify the people list screen displays without errors --> <target name="SearchPeople" description="Tests search for and displaying all people"> <webtest name="searchPeople"> &config; <steps> &login; <invoke stepid="click View People link" url="/editPerson.html?method=Search"/> <verifytitle stepid="we should see the personList title" text=".*${personList.title}.*" regex="true"/> </steps> </webtest> </target> 你将会希望添加"PersonTests"目标添加"SearchPeople"目标,这样这个测试就会和所有person相关的测试一起执行。 <!-- runs person-related tests --> <target name="PersonTests" depends="SearchPeople,EditPerson,SavePerson,AddPerson,DeletePerson" description="Call and executes all person test cases (targets)"> <echo>Successfully ran all Person JSP tests!</echo> </target> 现在你可以运行ant test-canoo -Dtestcase=SearchPeople (如果Tomcat没有运行,可以运行ant test-jsp),很有希望的结果是"BUILD SUCCESSFUL",如果如此 - 干得好! 8、菜单添加链接 最后一步是创建列表的add、edit和delete功能,最简单的方式是给web/pages/mainMenu.jsp添加一个新的链接: 注意: mainMenu.jsp中的其他链接没有使用,因此这个JSP可以在AppFuse中的多个框架之间共享(如Spring MVC和WebWork)。 <li> <html:link forward="viewPeople"> <fmt:message key="menu.viewPeople"/> </html:link> </li> menu.viewPeople是web/WEB-INF/classes/ApplicationResources.properties中的一个条目。 menu.viewPeople=View People 另一个选择(很可能的)是你会添加到菜单,为此,添加如下代码到web/WEB-INF/menu-config.xml: <Menu name="PeopleMenu" title="menu.viewPeople" forward="viewPeople"/> 确定以上的XML代码在<Menus>标签中,而不存在于另外一个<Menu>中,然后给web/pages/menu.jsp添加新的菜单 - 结果看起来如下: <%@ include file="/common/taglibs.jsp"%> <div id="menu"> <menu:useMenuDisplayer name="ListMenu" permissions="rolesAdapter"> <menu:displayMenu name="AdminMenu"/> <menu:displayMenu name="UserMenu"/> <menu:displayMenu name="PeopleMenu"/> <menu:displayMenu name="FileUpload"/> <menu:displayMenu name="FlushCache"/> <menu:displayMenu name="Clickstream"/> </menu:useMenuDisplayer> </div> 现在如果你运行ant clean deploy来启动Tomcat并且到http://localhost:8080/appfuse/mainMenu.html,你一定会看到如下的抓图。 注意在左边(来自mainMenu.jsp)和右边(来自menu.jsp)有了一个新的链接 就这样了! 你已经完成了使用AppFuse和Struts开发一组主控页面的整个生命周期 - 恭喜!现在是真正的测试你的应用没有错误的时候了,为了测试,停止tomcat然后运行ant clean test-all,这会运行项目里所有的测试,作为提醒,我们应该很容易的在琐碎代码中通过运行ant setup-db setup-tomcat test-all的设置和测试AppFuse,同样,如果你期望找到更稳定的例子 - 取出Struts Resume。 Happy Day! BUILD SUCCESSFUL Total time: 2 minutes 31 seconds </div> </div> <div class="col-md-3"> <div class="ui-box ut-pd20"> <div> 下载需要 <span style="font-size: 24px;">10</span> <span style="font-size: 14px;padding-right: 20px;color: #888888">金币</span> <a href="javascript:void(null);" onclick="JC.redirect('https://user.open-open.com/pay')" style="color: #cf6a07"> [金币充值 ] </a> <div class="kind-tip">亲,您也可以通过 <a href="javascript:void(0) " onclick="JC.redirect('/create')">分享原创文档</a> 来获得金币奖励!</div> </div> <div class="ut-mt20"> <a class="btn btn-block buy btn-danger download" href="javascript:void(null);" data-type="3" data-num="10" data-download="true"><i aria-hidden="true" class="fa fa-yen"></i> 下载文档</a> </div> </div> <div class="ui-box ut-pd10 sticky-top" > <div class="title clearfix"><h3>相关文档</h3></div> <ul class="ui-list"> <li class="ellipsis"><a href="/doc/123110a8bf3247388810dc58692d7b99.html"><i class="fa fa-file-word-o" aria-hidden="true"></i>  Appfuse 开发教程</a></li> <li class="ellipsis"><a href="/doc/9e09021fd11e4e7c8753cc0b527f6afa.html"><i class="fa fa-file-word-o" aria-hidden="true"></i>  Appfuse开发教程</a></li> <li class="ellipsis"><a href="/doc/e7c2e4f8116b401895c94261f24f9d7f.html"><i class="fa fa-file-word-o" aria-hidden="true"></i>  javafx2开发教程</a></li> <li class="ellipsis"><a href="/doc/beb212f220724581be35f3595fcc1bb2.html"><i class="fa fa-file-word-o" aria-hidden="true"></i>  GWT开发教程</a></li> <li class="ellipsis"><a href="/doc/e2211c1e9066414880d666cce65e6d18.html"><i class="fa fa-file-word-o" aria-hidden="true"></i>  Thrift 开发教程</a></li> <li class="ellipsis"><a href="/ppt/ed9bdcee34a1412294900a12d3d3950d.html"><i class="fa fa-file-powerpoint-o" aria-hidden="true"></i>  J2EE集成开发培训</a></li> <li class="ellipsis"><a href="/ppt/92d084cf87aa4b8181fffd8251b20624.html"><i class="fa fa-file-powerpoint-o" aria-hidden="true"></i>  Ext开发视频教程</a></li> <li class="ellipsis"><a href="/ppt/45e80dc66b7744f999a0b8d0beffeb0f.html"><i class="fa fa-file-powerpoint-o" aria-hidden="true"></i>  使用Emacs开发iPhone应用程序的教程</a></li> <li class="ellipsis"><a href="/ppt/a2bfdd6179ef46709f0ad5f020f361c1.html"><i class="fa fa-file-powerpoint-o" aria-hidden="true"></i>  java开发基础教程</a></li> <li class="ellipsis"><a href="/ppt/47d4097e4ba441599766493cc306efae.html"><i class="fa fa-file-powerpoint-o" aria-hidden="true"></i>  C语言开发实例教程</a></li> <li class="ellipsis"><a href="/pdf/aa86ef269a5a47a8983d4fbc0ff6e324.html"><i class="fa fa-file-pdf-o" aria-hidden="true"></i>  Appfuse 开发教程</a></li> <li class="ellipsis"><a href="/pdf/e593b50d4e3b4358b2a799e1ce1dee4a.html"><i class="fa fa-file-pdf-o" aria-hidden="true"></i>  Appfuse 教程 Appfuse开发</a></li> <li class="ellipsis"><a href="/pdf/827992c2a0754b69b3bd4d8e22e06201.html"><i class="fa fa-file-pdf-o" aria-hidden="true"></i>  Appfuse教程Appfuse开发</a></li> <li class="ellipsis"><a href="/pdf/f0011ceedb06466d9ab540eea7b30a2d.html"><i class="fa fa-file-pdf-o" aria-hidden="true"></i>  Java开发教程</a></li> <li class="ellipsis"><a href="/pdf/7952a3bb77b744eba0d51bb75cebd9b9.html"><i class="fa fa-file-pdf-o" aria-hidden="true"></i>  Cocos2d 开发教程</a></li> </ul> </div> </div> </div> </div> </article> <div class="container mt20"> <div class="row"> <!--main_list--> <div class="col-md-9 "> <div class=" box-line ut-pd20 ut-mt10"> <div id="reader-more"> <p class="title">下载文档到电脑,查找使用更方便</p> <p class="gray"> 文档的实际排版效果,会与网站的显示效果略有不同!!</p> <p class="download-info"> <span style="font-size: 14px;color: #888888">需要</span> <span style="font-size: 24px;">10</span> <span style="font-size: 14px;padding-right: 20px;color: #888888">金币</span> <a href="javascript:void(null);" onclick="JC.redirect('/create')" style="color: #cf6a07"> [ 分享文档获得金币 ] </a> <span class="fcff">0 人已下载</span> </p> <p> <a class="btn btn-danger download buy circle80 fs30" href="javascript:void(null);" data-type="3" data-num="10" data-download="true"><i aria-hidden="true" class="fa fa-yen"> </i> 下载文档</a> </p> </div> </div> </div> <!--right--> <div class="col-md-3"></div> </div> </div> </div> <footer > <div class="container py-5"> <div class="row"> <div class="col-md-3"> <h5>社区</h5> <div class="row"><div class="col-md-6"><a class="text-muted" href="/project/">项目</a></div><div class="col-md-6"><a class="text-muted" href="/solution/">问答</a></div><div class="col-md-6"><a class="text-muted" href="/wenku/">文库</a></div><div class="col-md-6"><a class="text-muted" href="/code/">代码</a></div><div class="col-md-6"><a class="text-muted" href="/lib/">经验</a></div><div class="col-md-6"><a class="text-muted" href="/news/">资讯</a></div></div> <ul class="list-unstyled text-small ut-mt20"><li><a class="text-muted" title=" 安卓开发专栏" target="_blank" href="http://www.open-open.com/lib/list/177">安卓开发专栏</a></li><li><a class="text-muted" href="http://www.open-open.com/lib/tag/开发者周刊" target="_blank" rel="tag">开发者周刊</a></li><li><a class="text-muted" href="http://www.open-open.com/lib/view/open1475497562965.html" target="_blank" rel="tag">Android Studio 使用推荐</a></li><li><a class="text-muted" href="http://www.open-open.com/lib/view/open1475497355674.html" target="_blank" rel="tag">Android开发推荐</a></li></ul> </div> <div class="col-md-3"> <h5>帮助中心</h5> <ul class="list-unstyled text-small"><li><a class="text-muted" href="/upload.html">文档上传须知</a></li></ul> <h5>关于我们</h5> <ul class="list-unstyled text-small"><li><a class="text-muted" href="/about.html">关于深度开源</a></li><li><a class="text-muted" href="/duty.html">免责声明</a></li><li><a class="text-muted" href="/contact.html">联系我们</a></li></ul> </div> <div class="col-md-6 text-center"><img class=center-block src="https://static.open-open.com/img/logo01.svg" width=190px alt="深度开源"><small class="d-block mb-3 text-muted ut-mt40">© 2006-2019 深度开源 —— 开源项目,开源代码,开源文档,开源新闻,开源社区  杭州精创信息技术有限公司  <br/><br/><img src="https://static.open-open.com/img/beian.png"/><a target="_blank" href="http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=33010602002439">  浙公网安备 33010602002439号</a>  <a target="_blank" href="https://www.miibeian.gov.cn/">浙ICP备09019653号-31</a></small></div> </div> </div> </footer> <div id="fTools"><span id="gotop"> <i class="fa fa-arrow-up" aria-hidden="true"></i> </span><span id="feedback" title="建议反馈"> <i class="fa fa-inbox" aria-hidden="true"></i></span></div> <!-- Bootstrap core JavaScript ================================================== --> <!-- Placed at the end of the document so the pages load faster --> <script type="text/javascript" src="https://static.open-open.com/js/lib.js"></script> <script type="text/javascript" src="https://static.open-open.com/assets/jquery-confirm/jquery-confirm.js?v=4.7.0"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"></script> <script src="https://static.open-open.com/js/bootstrap.min.js"></script> <script type="text/javascript" src="https://static.open-open.com/js/base.js?v=2.002"></script> <script type="text/javascript" src="https://static.open-open.com/js/jq-plug.js?v=2.002"></script> <script> $(function () { JC.reminderPop();//弹出用户信息 $(".link-login").click(function(){ JC.lORr('login'); }); $("#topSearch").searchInit(); //用户登录状态 JC.setLogin(false); }); </script> <!-- JavaScript at the bottom for fast page loading --> <!-- end scripts --> </body> </html>