一步一步開發
Spring Framework MVC
應用程序
作者:Thomas Risberg
2003年7月(2005年4月修訂)
翻譯 Shining Ray @ Nirvana Studio
這是一個關于如何使用Spring Framework從無到有開發一個Web應用的逐步的指南。本文分成幾個部分。你可以按順序閱讀或者根據你對他們的熟悉程度,跳過某些章節。
目錄
第 17 步 – 修改視圖用于現實業務數據并且添加消息綁定的支持
第19步 – 添加消息綁定以及給build.xml添加“clean”目標
第20步 – 為SpringappController添加單元測試
第21步 – 為ProductManager添加單元測試和新的功能
第1部分 -設置基本應用程序和環境
先決條件:
o Java SDK(我目前使用的是1.4.2版)
o Ant (使用1.6.2)
o Apache Tomcat(使用5.0.28版)
你應該已經對使用以上軟件相當的自如了。
我不會在這篇文檔里面涵蓋很多背景信息或者理論——已經有很多書深入地討論了這些東西。我們會直接投入開發程序的過程中。
第1步 - 開發目錄
我們需要一個地方用來放置所有的源代碼和其他我們將要創建的文件,所以我新建了一個目錄,并命名為“springapp
”。你可以把這個目錄放在你的主文件夾或者其它一些地方。我把我的新建在我已經放在主目錄中的“projects
”目錄下,這時我的目錄的完整路徑“/User/trisberg/projects/springapp
”。在這個目錄中我新建了一個“src”目錄來存放所有的Java源代碼。然后我創建了另一個目錄并命名為“war”。這個目錄會存放所有將來進入WAR文件的東西,這個文件我們可以用來部署我們的應用程序。所有除了Java源代碼的源文件,像JSP文件和配置文件,也屬于這個目錄。
第2步 – index.jsp
我將從建立一個叫做“index.jsp
”的文件(放在war目錄中)開始。這是我們整個應用的入口點。
springapp/war/index.jsp |
|
只是為了Web應用的完整性,我在war目錄中的WEB-INF目錄中創建了一個web.xml。
springapp/war/WEB-INF/web.xml |
|
第3步 – 將應用程序部署到Tomcat
下面,我要寫一個Ant構建腳本,貫穿這個文檔我們都要使用它。一個獨立的構建腳本包含了應用服務器特定的任務。同樣還有用于控制Tomcat下的任務。
springapp/build.xml |
<!-- We need the servlet API classes: -->
<!-- for Tomcat 4.1 use servlet.jar -->
<!-- for Tomcat 5.0 use servlet-api.jar -->
<!-- for Other app server - check the docs -->
<!-- ============================================================== -->
<!-- Tomcat tasks - remove these if you don't have Tomcat installed -->
<!-- ============================================================== -->
<!-- End Tomcat tasks -->
|
這個腳本現在包含了所有我們需要的目標,以便使我們開發更加容易。這里我不會詳細解釋這個腳本,因為大部分內容都是比較標準Ant和Tomcat的東西。你可以直接復制上面的構建文件并且把它放在你的開發目錄的根目錄中。我們還需要一個build.properties
文件,你需要自定這個文件來配合你的服務器安裝。這個文件和build.xml
文件在同一個目錄中。
springapp/build.properties |
# Ant properties for building the springapp
|
如果你是在一個你不是Tomcat安裝的所有者的系統中,那么Tomcat所有者必須給你訪問webapps目錄的全部權限,或者他可以在webapps目錄下面新建一個“springapp”目錄,并且給你全部權限來把程序部署到這個新建的目錄中。在Linux上我運行chmod a+rwx springapp
來給與所有人對目錄的訪問權利。
如果你使用一個不用的Web應用服務器,那么你要刪除在構建腳本底部的那些特定于Tomcat的任務。你還要依賴你服務器的熱部署特定,否則你就需要手工重新啟動你的應用服務器。
現在我運行Ant來確保所有的東西都工作正常。你應該把你當前的目錄設置到“springapp”目錄下。
[trisberg@localhost springapp]$ ant
Buildfile: build.xml
usage:
[echo] springapp build file
[echo] -----------------------------------
[echo] Available targets are:
[echo] build --> Build the application
[echo] deploy --> Deploy application as directory
[echo] deploywar --> Deploy application as a WAR file
[echo] install --> Install application in Tomcat
[echo] reload --> Reload application in Tomcat
[echo] start --> Start Tomcat application
[echo] stop --> Stop Tomcat application
[echo] list --> List Tomcat applications
BUILD SUCCESSFUL
Total time: 2 seconds
|
這里最后的動作是進行實際的部署。只要運行Ant并且指明“deploy”或者“deploywar”作為目標。
[trisberg@localhost springapp]$ ant deploy
Buildfile: build.xml
build:
[mkdir] Created dir: /Users/trisberg/projects/springapp/war/WEB-INF/classes
deploy:
[copy] Copying 2 files to /Users/trisberg/jakarta-tomcat-5.0.28/webapps/springapp
BUILD SUCCESSFUL
Total time: 2 seconds
|
第4步 – 測試應用
讓我們立刻啟動Tomcat并且確保我們可以訪問這個應用程序。使用我們的構建腳本中的“list
”任務來查看Tomcat是否已經載入了新的應用程序。
[trisberg@localhost springapp]$ ant list
Buildfile: build.xml
list:
[list] OK - Listed applications for virtual host localhost
[list] /admin:running:0:/Users/trisberg/jakarta-tomcat-5.0.28/server/webapps/admin
[list] /webdav:running:0:/Users/trisberg/jakarta-tomcat-5.0.28/webapps/webdav
[list] /servlets-examples:running:0:/Users/trisberg/jakarta-tomcat-5.0.28/webapps/servlets-examples
[list] /springapp:running:0:/Users/trisberg/jakarta-tomcat-5.0.28/webapps/springapp
[list] /jsp-examples:running:0:/Users/trisberg/jakarta-tomcat-5.0.28/webapps/jsp-examples
[list] /balancer:running:0:balancer
[list] /tomcat-docs:running:0:/Users/trisberg/jakarta-tomcat-5.0.28/webapps/tomcat-docs
[list] /:running:0:/Users/trisberg/jakarta-tomcat-5.0.28/webapps/ROOT
[list] /manager:running:0:/Users/trisberg/jakarta-tomcat-5.0.28/server/webapps/manager
BUILD SUCCESSFUL
Total time: 1 second
|
如果他沒有被列出,使用“install
”任務來把應用程序安裝到Tomcat中。
[trisberg@localhost springapp]$ ant install
Buildfile: build.xml
install:
[install] OK - Installed application at context path /springapp
BUILD SUCCESSFUL
Total time: 2 seconds
|
現在打開一個瀏覽器并瀏覽http://localhost:8080/springapp/index.jsp.
第5步 – 下載Spring分發包
如果你還沒有下載Spring Framework的發布文件,那現在就行動吧。我目前使用的是“spring-framework-1.2-with-dependencies.zip
”,可以從http://www.springframework.org/download.html 下載到。我把文件解壓縮到我的主目錄中。我們后面將要用到里面的一些文件。
到此為止必要的環境安裝已經完成了,現在我們要開始實際開發我們的Spring Framework MVC應用了。
第6步 – 修改WEB-INF目錄中的web.xml
進入“springapp/war/ WEB-INF
”目錄。修改我們前面創建的最小“web.xml
”文件。現在我們要修改它來滿足我們需求。我們定義一個將來控制我們所有請求轉向的DispatcherServlet
,它將根據我們以后某處輸入的信息進行工作。同時還有一個標準的用來映射到我們使用的URL模式的servlet-mapping
條目。我決定讓所有帶“.htm
”擴展名的URL轉向到“springapp
”分配器。
springapp/war/WEB-INF/web.xml |
<servlet>
<servlet-name>springapp</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springapp</servlet-name>
<url-pattern>*.htm</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>
index.jsp
</welcome-file>
</welcome-file-list>
|
下面,在springapp/war/WEB-INF
目錄下創建一個叫做“springapp-servlet.xml
”的文件(你可以直接從Spring分發包中復制一個范例文件,位于sample/skeletons/webapp-minimal
目錄中)。DispatcherServlet
所使用的定義就要放在這個文件中。文件名是web.xml
中的servlet-name并加上“-servlet”后綴。這是Spring Framework所使用的標準命名約定。現在,添加一個叫做springappController
的bean條目并創建一個SpringappController
類。這里將定義我們的應用程序所使用的控制器。我們還要添加一個URL映射 urlMapping
這樣DispatcherServlet
就會知道對于不同的URL應該調用哪個控制器。
springapp/war/WEB-INF/springapp-servlet.xml |
<!--
- Application context definition for "springapp" DispatcherServlet.
-->
|
第7步 - 把jar文件復制到WEB-INF/lib
首先在“war/WEB-INF
”目錄中創建一個“lib”目錄。然后,從Spring分發包中,將spring.jar
(spring-framework-1.2/dist/spring.jar
)復制到新建的war/WEB-INF/lib
目錄中。同時把commons-logging的jar文件(spring-framework-1.2/lib/jakarta-commons/commons-logging.jar
)也復制到war/WEB-INF/lib
中。同時我們還需要log4j.jar。把log4j-1.2.9.jar(spring-framework-1.2/lib/log4j/log4j-1.2.9.jar)
復制到 war/WEB-INF/lib
目錄。這些jar文件以后會被部署到服務器上而且他們在構建過程中也會被用到。
第8步 - 創建你的控制器
創建你的控制器——我把我的控制器命名為SpringappController.java
并把它放在springapp/src
目錄下。
springapp/src/SpringappController.java |
|
這是非常基本的控制器。我們稍后會對他進行擴充,同時過會兒我們還要擴展一些已經提供的抽象的基本實現。這個控制器處理請求并返回一個ModelAndView
。不過我們還沒有定義任何視圖,所以現在沒什么可做的了。
第9步 - 構建應用程序
運行build.xml
中的“build
”任務。基本上代碼應該順利通過編譯。
[trisberg@localhost springapp]$ ant build
Buildfile: build.xml
build:
[javac] Compiling 1 source file to /Users/trisberg/projects/springapp/war/WEB-INF/classes
BUILD SUCCESSFUL
Total time: 2 seconds
|
第10步 – 復制并修改log4j.properties
Spring Framework使用log4j來進行日志記錄,所以我們要為log4j創建一個配置文件。把log4j.properties
文件從Petclinic范例應用程序(spring-framework-1.2/samples/petclinic/war/WEB-INF/log4j.properties
) 中復制到war/WEB-INF/classes
目錄中(這個目錄應該在前一步中被創建了)。現在取消log4j.rootCategory
屬性前的注釋并且更改寫入的日志文件的名稱和位置。我決定把日志寫入與其他Tomcat日志一樣的目錄中。
springapp/war/WEB-INF/classes/log4j.properties |
# For JBoss: Avoid to setup Log4J outside
# For all other servers: Comment out the Log4J listener in web.xml to activate Log4J.
|
第11步 – 部署應用程序
運行build.xml
中的“deploy
”任務然后再運行“stop
”和“start
”任務。這將強制應用程序重新載入。我們要檢查Tomcat日志中的部署錯誤——可能在上面的XML文件中有輸入錯誤或者也可能缺少class或者jar文件。下面是一個日志的例子(/Users/trisberg/jakarta-tomcat-5.0.28/logs/springapp.log
)。
2005-04-24 14:58:18,112 INFO [org.springframework.web.servlet.DispatcherServlet] - Initializing servlet 'springapp'
2005-04-24 14:58:18,261 INFO [org.springframework.web.servlet.DispatcherServlet] - FrameworkServlet 'springapp': initialization started
2005-04-24 14:58:18,373 INFO [org.springframework.beans.factory.xml.XmlBeanDefinitionReader] - Loading XML bean definitions from ServletContext resource [/WEB-INF/springapp-servlet.xml]
2005-04-24 14:58:18,498 INFO [org.springframework.web.context.support.XmlWebApplicationContext] - Bean factory for application context [WebApplicationContext for namespace 'springapp-servlet']: org.springframework.beans.factory.support.DefaultListableBeanFactory defining beans [springappController,urlMapping]; root of BeanFactory hierarchy
2005-04-24 14:58:18,505 INFO [org.springframework.web.context.support.XmlWebApplicationContext] - 2 beans defined in application context [WebApplicationContext for namespace 'springapp-servlet']
2005-04-24 14:58:18,523 INFO [org.springframework.core.CollectionFactory] - JDK 1.4+ collections available
2005-04-24 14:58:18,524 INFO [org.springframework.core.CollectionFactory] - Commons Collections 3.x available
2005-04-24 14:58:18,537 INFO [org.springframework.web.context.support.XmlWebApplicationContext] - Unable to locate MessageSource with name 'messageSource': using default [org.springframework.context.support.DelegatingMessageSource@8dacb]
2005-04-24 14:58:18,539 INFO [org.springframework.web.context.support.XmlWebApplicationContext] - Unable to locate ApplicationEventMulticaster with name 'applicationEventMulticaster': using default [org.springframework.context.event.SimpleApplicationEventMulticaster@5674a4]
2005-04-24 14:58:18,549 INFO [org.springframework.ui.context.support.UiApplicationContextUtils] - No ThemeSource found for [WebApplicationContext for namespace 'springapp-servlet']: using ResourceBundleThemeSource
2005-04-24 14:58:18,556 INFO [org.springframework.beans.factory.support.DefaultListableBeanFactory] - Pre-instantiating singletons in factory [org.springframework.beans.factory.support.DefaultListableBeanFactory defining beans [springappController,urlMapping]; root of BeanFactory hierarchy]
2005-04-24 14:58:18,557 INFO [org.springframework.beans.factory.support.DefaultListableBeanFactory] - Creating shared instance of singleton bean 'springappController'
2005-04-24 14:58:18,603 INFO [org.springframework.beans.factory.support.DefaultListableBeanFactory] - Creating shared instance of singleton bean 'urlMapping'
2005-04-24 14:58:18,667 INFO [org.springframework.web.servlet.DispatcherServlet] - Using context class [org.springframework.web.context.support.XmlWebApplicationContext] for servlet 'springapp'
2005-04-24 14:58:18,668 INFO [org.springframework.web.servlet.DispatcherServlet] - Unable to locate MultipartResolver with name 'multipartResolver': no multipart request handling provided
2005-04-24 14:58:18,670 INFO [org.springframework.web.servlet.DispatcherServlet] - Unable to locate LocaleResolver with name 'localeResolver': using default [org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver@318309]
2005-04-24 14:58:18,675 INFO [org.springframework.web.servlet.DispatcherServlet] - Unable to locate ThemeResolver with name 'themeResolver': using default [org.springframework.web.servlet.theme.FixedThemeResolver@c11e94]
2005-04-24 14:58:18,681 INFO [org.springframework.web.servlet.DispatcherServlet] - No HandlerAdapters found in servlet 'springapp': using default
2005-04-24 14:58:18,700 INFO [org.springframework.web.servlet.DispatcherServlet] - No ViewResolvers found in servlet 'springapp': using default
2005-04-24 14:58:18,700 INFO [org.springframework.web.servlet.DispatcherServlet] - FrameworkServlet 'springapp': initialization completed in 439 ms
2005-04-24 14:58:18,704 INFO [org.springframework.web.servlet.DispatcherServlet] - Servlet 'springapp' configured successfully
|
第12步 - 創建一個視圖
現在是時候創建我們第一個視圖了。我將使用一個JSP頁面,并命名為hello.jsp
。然后我把它放在了war目錄中。
springapp/war/hello.jsp |
|
里面沒什么奇特的東西,只是為了現在試一下。下面我們要修改SpringappController來引導到這個視圖。
springapp/src/SpringappController.java |
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());
logger.info("SpringappController - returning hello view");
|
當我在修改這個類的同時,我還添加了一個logger
這樣我們可以校對我們在這里實際得到的值。更改的內容將以紅色標明。這個類返回的模型將最終通過一個ViewResolver
來進行轉換。由于我們并沒有指定一個特別的,所以我們將使用一個默認的,它僅僅引導到匹配指定的視圖的名字的URL。我們稍候將修改它。
現在編譯并部署這個應用程序。在通知Tomcat重新啟動應用程序之后,所有的東西都應該被重新載入了。
讓我們在瀏覽器中試一下——輸入URL http://localhost:8080/springapp/hello.htm,然后我們應該看到以下內容:
我們也可以檢查一下日志——我這里僅列出最后的條目,我們可以看到控制器確實被調用了,然后它引導到了hello視圖。(
/Users/trisberg/jakarta-tomcat-5.0.28/logs/springapp.log
)
2005-04-24 15:01:56,217 INFO [org.springframework.web.servlet.DispatcherServlet] - FrameworkServlet 'springapp': initialization completed in 372 ms
2005-04-24 15:01:56,217 INFO [org.springframework.web.servlet.DispatcherServlet] - Servlet 'springapp' configured successfully
2005-04-24 15:03:57,908 INFO [SpringappController] - SpringappController - returning hello view
|
總結
讓我們快速回顧一下目前我們已經創建的應用程序的各個部分:
1. 一個基本不做什么事情的介紹頁面index.jsp
。它只是用來測試我們的安裝。我們以后會修改它,以便提供一個鏈接指向我們的應用。
2. 一個DispatcherServlet
和一個相應的配置文件springapp-servlet.xml
。
3. 一個控制器 springappController.java
,包含了有限的功能——他僅僅把一個ModelAndView
引導到ViewResolver
。事實上,我們目前還只有一個空的模型,不過我們以后會修正它。
4. 一個視圖 hello.jsp
,同樣是極其基本的。但是整個安裝工作可以運行并且我們現在已經準備好開始添加更多的功能了。
第2部分 -開發和配置應用程序
在第一部分(第1 – 12 步)我們已經配置了開發環境并建立了一個基本的應用程序。
我們已經準備好了:
1. 一個介紹頁面index.jsp
.
2. 一個 DispatcherServlet
,以及相應的配置文件
springapp-servlet.xml
3. 一個控制器 springappController.java
.
4. 一個視圖 hello.jsp
.
現在我們要改進這些部件來建立一個更好的應用程序。
第13步 – 改進index.jsp
我們將利用JSP標準標簽庫(JSTL),所以我要先復制我們所需的JSTL文件到我們的WEB-INF/lib
目錄中。復制“spring-framework-1.2/lib/j2ee
”中的jstl.jar和“spring-framework-1.2/lib/jakarta-taglibs
”中的standard.jar
到springapp/war/WEB-INF/lib
目錄下。我還創建了一個“header”文件,將來會在我寫的每一個JSP頁面中包含這個文件。這樣會令開發更加簡單同時我可以確保在所有的JSP文件中都有同樣的定義。我將把所有的JSP文件放在WEB-INF
目錄下的一個jsp
目錄中。這可以確保只有控制器可以訪問這些視圖——直接在瀏覽器中輸入URL來訪問這些頁面是不行的。這個策略不一定在所有的應用服務器中都可以行得通,如果你使用的應用服務器恰好不行的話,只要把jsp目錄往上移一級。你可以使用springapp/war/jsp
作為目錄來替代以后所有的例子代碼中的“springapp/war/WEB-INF/jsp
”。
springapp/war/WEB-INF/jsp/include.jsp |
|
現在我們可以更改index.jsp
來使用,由于我們使用了JSTL,我們可以使用<c:redirect>
標簽來轉向到我們的控制器。
springapp/war/index.jsp |
|
第14步 – 改進視圖和控制器
現在我要把hello.jsp
視圖移入WEB-INF/jsp
目錄。Index.jsp
里面添加的包含文件include.jsp
同樣也添加到了hello.jsp
中。我也使用JSTL<c:out>
標簽來輸出從傳給視圖的模型里獲取的當前的日期和時間。
springapp/war/WEB-INF/jsp/hello.jsp |
|
對于SpringappController.java
,我們還要做一些更改。由于我們把文件移動到了一個新的位置,所以需要把視圖變成WEB-INF/jsp/hello.jsp
。同時添加一個包含當前時間和日期的字符串作為模型。
springapp/src/SpringappController.java |
/** Logger for this class and subclasses */
String now = (new java.util.Date()).toString();
logger.info("returning hello view with " + now);
return new ModelAndView("WEB-INF/jsp/hello.jsp", "now", now);
|
在我們構建并部署了新的代碼之后,現在我們準備嘗試它了。我們在瀏覽器中輸入http://localhost:8080/springapp,它首先會調用index.jsp
,然后它又會重定向到hello.htm
,這個URL又會調用控制器并把時間和日期發送給視圖。
第15步 – 解耦視圖和控制器
現在控制器是指明了視圖的完整路徑,這在控制器和視圖之間產生了一個多余的依賴關系。理想上來說,我們要使用一個邏輯名稱來映射到視圖,這可以讓我們無需更改控制器就可以切換視圖。你可以在一個屬性文件中設置這個映射,如果你喜歡使用ResourceBundleViewResolver
和SimpleUrlHandlerMapping
類的話。如果你的映射需求確實很簡單,那么在InternalResourceViewResolver
上加上前綴和后綴會很方便。后一種方法就是我現在要實現的。我修改了springapp-servlet.xml
并包含了viewResolver
條目。我選擇使用JstlView
,它可以讓我們使用JSTL,可以結合消息資源綁定,同時他還可以支持國際化。
springapp/war/WEB-INF/springapp-servlet.xml |
<!--
- Application context definition for "springapp" DispatcherServlet.
-->
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass">
<value>org.springframework.web.servlet.view.JstlView</value>
</property>
<property name="prefix"><value>/WEB-INF/jsp/</value></property>
<property name="suffix"><value>.jsp</value></property>
</bean>
|
所以現在我可以從控制器的視圖名稱中刪除前綴和后綴了。
springapp/src/SpringappController.java |
return new ModelAndView("hello", "now", now);
|
編譯并部署,應用程序應該仍然可以正常運行。
第16步 – 添加一些業務邏輯的類
目前位置我們的應用還不是很有用。我想添加一些業務邏輯,一個Product
類和一個管理所有產品的類。我把管理類命名為ProductManager
。為了能分離依賴Web的邏輯和業務邏輯,我將在Java源代碼重創建兩個單獨的包——web和bus。如果這個應用程序是為一個真實的公司開發的,我可能會把包命名成像com.mycompany.web
和com.mycompany.bus
之類的名字,不過這只是一個演示而已我就讓包的名稱簡短一些。Product
類是實現為一個JavaBean——它有一個默認的構造器(如果我們沒有指明任何構造器,會自動給出),兩個實例變量description
和price
的獲取器(getter)和設制器(setter)。我還把它設為Serializable
,這對我們的應用不是必需的,不過以后我們很可能要把這個類在不同的應用層中傳遞的時候,那時就可以直接使用了。
springapp/src/bus/Product.java |
|
ProductManager
中有一個Product
的列表List,同樣的,這個類也是實現為一個JavaBean。
springapp/src/bus/ProductManager.java |
|
下面,我修改了SpringappController
來存放一個指向ProductManager
類的引用。正如你所見,它現在在一個單獨的web的包中——記得把代碼放到這個新位置中。我還要添加讓控制器將產品信息傳送到視圖的代碼。getModelAndView
現在返回一個Map
,同時包含了時間日期和產品管理的引用。
springapp/src/web/SpringappController.java |
import java.util.Map;
import java.util.HashMap;
private ProductManager prodMan;
Map myModel = new HashMap();
myModel.put("now", now);
myModel.put("products", getProductManager().getProducts());
return new ModelAndView("hello", "model", myModel);
public void setProductManager(ProductManager pm) {
prodMan = pm;
}
public ProductManager getProductManager() {
return prodMan;
}
|
第 17 步 – 修改視圖用于現實業務數據并且添加消息綁定的支持
我使用了JSTL<c:forEach>
標簽來添加了一個現實產品信息的部分。我還用JSTL<fmt:message>
標記替換了標題和歡迎文本,這樣可以從給定的“message”源中讀取文本并顯示——在后面的步驟中我會顯示這個方法。
springapp/war/WEB-INF/jsp/hello.jsp |
<h1><fmt:message key="heading"/></h1>
<h3>Products</h3>
<c:forEach items="${model.products}" var="prod">
<c:out value="${prod.description}"/> <i>$<c:out value="${prod.price}"/></i><br><br>
</c:forEach>
|
第18步 – 添加一些測試數據來自動組裝一些業務對象
我不會添加任何用于從數據庫中載入業務對象的代碼。然和,我們可以使用Spring的bean和應用程序上下文的支持來牽線到實例的引用。我只要簡單地把握需要的數據作為bean之間的偶合條目寫入springapp-servlet.xml
。我還要添加messageSource
條目來引入消息資源綁定(“messages.properties
”),在下一步我將創建它。
springapp/war/WEB-INF/springapp-servlet.xml |
<!--
- Application context definition for "springapp" DispatcherServlet.
-->
<property name="productManager">
<ref bean="prodMan"/>
</property>
</bean>
<bean id="prodMan" class="bus.ProductManager">
<property name="products">
<list>
<ref bean="product1"/>
<ref bean="product2"/>
<ref bean="product3"/>
</list>
</property>
</bean>
<bean id="product1" class="bus.Product">
<property name="description"><value>Lamp</value></property>
<property name="price"><value>5.75</value></property>
</bean>
<bean id="product2" class="bus.Product">
<property name="description"><value>Table</value></property>
<property name="price"><value>75.25</value></property>
</bean>
<bean id="product3" class="bus.Product">
<property name="description"><value>Chair</value></property>
<property name="price"><value>22.79</value></property>
</bean>
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename"><value>messages</value></property>
</bean>
|
第19步 – 添加消息綁定以及給build.xml添加“clean”目標
我在war/WEB-INF/classes
目錄中創建了一個“messages.properties
”文件。這個屬性綁定文件目前有3個條目可以匹配在<fmt:message>
標記中指定的鍵。
springapp/war/WEB-INF/classes/messages.properties |
|
由于我們移動了一些源代碼,所以給構建腳本中添加一個“clean
”和一個“undeploy
”目標。我把以下內容添加到build.xml文件。
<target name="clean" description="Clean output directories">
<delete>
<fileset dir="${build.dir}">
<include name="**/*.class"/>
</fileset>
</delete>
</target>
<target name="undeploy" description="Un-Deploy application">
<delete>
<fileset dir="${deploy.path}/${name}">
<include name="**/*.*"/>
</fileset>
</delete>
</target>
|
現在停止Tomcat服務器,運行clean
、undeploy
和deploy
目標。這應該會刪除所有的舊文件,重新構建應用并部署它:
第3部分 -為應用程序添加單元測試和表單
第20步 – 為SpringappController
添加單元測試
在我們創建單元測試之前,我們要先準備Ant并讓我們的構建腳本可以處理單元測試。Ant有一個內置的JUnit目標,但是我們需要把junit.jar
放入Ant的lib目錄中。我使用了Spring分發包中自帶的spring-framework-1.2/lib/junit/junit.jar
。只要把它復制到你的Ant安裝目錄的lib目錄下即可。我還將以下目標添加到我們構建腳本中:
<target name="junit" depends="build" description="Run JUnit Tests">
<junit printsummary="on"
fork="false"
haltonfailure="false"
failureproperty="tests.failed"
showoutput="true">
<classpath refid="master-classpath"/>
<formatter type="brief" usefile="false"/>
<batchtest>
<fileset dir="${build.dir}">
<include name="**/Test*.*"/>
</fileset>
</batchtest>
</junit>
<fail if="tests.failed">
tests.failed=${tests.failed}
***********************************************************
***********************************************************
**** One or more tests failed! Check the output ... ****
***********************************************************
***********************************************************
</fail>
</target>
|
現在我在src
目錄中添加了一個新的子目錄叫做tests
。相信大家也都猜到了,這個目錄將包含所有的單元測試。
這些工作結束之后,我們準備開始寫我們的第一個單元測試。SpringappController
要依賴于HttpServletRequest
和HttpServletResponse
以及我們的應用程序上下文。由于控制器并沒有使用請求和響應,我們直接傳送null
。如果不是這樣,我們要使用EasyMock創建一些模仿對象mock object,這樣就可以在測試用使用了。使用某個類,可以在Web Server環境之外載入應用程序上下文。有好幾個類可以使用,針對當前的任務我們將使用FileSystemXmlApplicationContext
。
springapp/src/tests/TestSpringappController.java |
|
唯一的測試就是調用handleRequest
,我們檢測從模型中返回的產品。在setUp
方法中,我們載入應用程序上下文,之前我已經復制到了tests
中的WEB-INF
目錄中。我創建了一個副本這樣這個文件可以在測試中以“messageSource
”所需的bean的最小集來運行。這樣,復制springapp/war/WEB-INF/springapp-servlet.xml
到springapp/src/tests/WEB-INF
目錄中。你可以刪除“messageSource
”、“urlMapping
”和“viewResolver
”bean條目,因為這個測試不需要他們。
springapp/src/tests/WEB-INF/springapp-servlet.xml |
<!--
- Application context definition for "springapp" DispatcherServlet.
-->
|
當你運行這個測試的時候,你應該看到載入應用程序上下文時有很多日志信息。
第21步 – 為ProductManager添加單元測試和新的功能
接下來我為ProductManager
添加一個測試案例,同時我打算給ProductManager
添加一個用于增加價格的新方法,并添加一個測試。
springapp/src/tests/TestProductManager .java |
|
對于這個測試,沒有必要創建一個應用程序上下文。我只在setUp
方法中建立了產品信息并且把他們添加到了產品管理對象中。我還給getProducts
和increasePrice
添加了測試。increasePrice
方法根據傳給它的百分比對價格進行增加。我修改了ProductManager
類來實現這個新方法。
springapp/src/bus/ProductManager.java |
import java.util.ListIterator;
public void increasePrice(int pct) {
ListIterator li = products.listIterator();
while (li.hasNext()) {
Product p = (Product) li.next();
double newPrice = p.getPrice().doubleValue() * (100 + pct)/100;
p.setPrice(new Double(newPrice));
}
}
|
下面我構建并運行這些測試。正如你所見,這些測試就像一般的測試一樣——業務類不依賴于任何servlet類,所以這些類測試起來很方便。
第22步 – 添加一個表單
為了在Web應用中提供了一個接口,我添加了一個可以讓用戶輸入百分比值的表單。這個表單使用了一個叫做“spring”的標簽庫,它是由Spring Framework所提供的。我們要從Spring的分發包中把spring-framework-1.2/dist/spring.tld
復制到springapp/war/WEB-INF
目錄中。現在我們還要給web.xml
添加一個<taglib>
條目。
springapp/war/WEB-INF/web.xml |
<taglib>
<taglib-uri>/spring</taglib-uri>
<taglib-location>/WEB-INF/spring.tld</taglib-location>
</taglib>
|
我們還需要在jsp文件的page
指令中申明這個taglib。我們用普通的方法通過<form>
標簽聲明一個表單,以及一個<input>
文本域和一個提交按鈕。
springapp/war/WEB-INF/jsp/priceincrease.jsp |
|
<spring:bind>
標記是用于將一個<input>
表單元素綁定到一個命令對象PriceIncrease.java
上的。這個命令對象以后會被傳送給效驗器,同時如果它通過了檢驗,它會被繼續傳送給控制器。${status.errorMessage}
和${status.value}
是由框架聲明的特殊變量,可以用來顯示錯誤信息和當前域的值。
springapp/src/bus/PriceIncrease.java |
|
這是一個十分簡單的JavaBean類,同時這里有一個屬性以及他的獲取器和設置器。在用戶按下了提交按鈕之后,Validator
類將獲取控制。在表單中輸入的值會被框架設置在命令對象上。然后會調用方法validate
,并傳入命令對象和一個用來存放錯誤信息的對象。
springapp/src/bus/PriceIncreaseValidator.java |
|
現在我們要在springapp-servlet.xml
文件中添加一條內容來定義新的表單和控制器。我們定義命令對象和效驗器的屬性。我們還要指明兩個視圖,一個用來顯示表單,另一個將是在成功的表單處理之后我們將看到的。后一個也叫做成功視圖,可以是兩種類型之一:它可以是一個普通的視圖引用直接引導到我們某個JSP頁面。但這種方法的一個缺點是,如果用戶刷新頁面,那么表單的數據就會被重新提交,然后你可能最后就做了兩次priceIncreace
。另一種方法是使用一個重定向,它將給用戶瀏覽器返回一個應答并且指示瀏覽器重定向到一個新的URL。我們這里使用的這個URL不可以是我們的JSP頁面之一,因為他們對于直接訪問是不可見的。必須一個從外部可以獲取的URL。所以我選擇了“hello.htm
”來作為我的重定向URL。這個URL影射到“hello.jsp
”頁面,這個應該運行得很令人滿意。
springapp/war/WEB-INF/springapp-servlet.xml |
<!-- Controller for the initial "Hello" page -->
<!-- Validator and Form Controller for the "Price Increase" page -->
<bean id="priceIncreaseValidator" class="bus.PriceIncreaseValidator"/>
<bean id="priceIncreaseForm" class="web.PriceIncreaseFormController">
<property name="sessionForm"><value>true</value></property>
<property name="commandName"><value>priceIncrease</value></property>
<property name="commandClass"><value>bus.PriceIncrease</value></property>
<property name="validator"><ref bean="priceIncreaseValidator"/></property>
<property name="formView"><value>priceincrease</value></property>
<property name="successView"><value>hello.htm</value></property>
<property name="productManager">
<ref bean="prodMan"/>
</property>
</bean>
<prop key="/priceincrease.htm">priceIncreaseForm</prop>
|
下面,讓我們看一下這個表單的控制器。onSubmit
方法獲取了控制并且在它調用ProductManager
對象的increasePrice
方法之前進行了一些日志記錄。然后它使用successView
的url創建了RedirectView
的一個新的實例,并傳遞這個實例給ModelAndView
,最后返回這個ModelAndView
的實例。
springapp/src/web/PriceIncreaseFormController.java |
|
我們還要在message.properties
資源文件里面添加一些消息。
springapp/war/WEB-INF/classes/messages.properties |
priceincrease.heading=Price Increase :: SpringApp
error.not-specified=Percentage not specified!!!
error.too-low=You have to specify a percentage higher than {0}!
error.too-high=Don't be greedy - you can't raise prices by more than {0}%!
required=Entry required.
typeMismatch=Invalid data.
typeMismatch.percentage=That is not a number!!!
|
最后,我們要從hello.jsp
中提供一個指向priceincrease
頁面的鏈接。