Spring MVC step-by-step1
来源:百度文库 编辑:神马文学网 时间:2024/05/13 15:37:15
一步一步开发
Spring Framework MVC
应用程序
作者:Thomas Risberg
2003年7月(2005年4月修订)
翻译 Shining Ray @Nirvana Studio
这是一个关于如何使用Spring Framework从无到有开发一个Web应用的逐步的指南。本文分成几个部分。你可以按顺序阅读或者根据你对他们的熟悉程度,跳过某些章节。
目录
第1部分 - 设置基本应用程序和环境.... 2
第1步 - 开发目录... 2
第2步 – index.jsp. 2
第3步 – 将应用程序部署到Tomcat.. 2
第4步 – 测试应用... 6
第5步 – 下载Spring分发包... 7
第6步 – 修改WEB-INF目录中的web.xml. 7
第7步 - 把jar文件复制到WEB-INF/lib.. 8
第8步 - 创建你的控制器... 9
第9步 - 构建应用程序... 9
第10步 – 复制并修改log4j.properties. 9
第11步 – 部署应用程序... 10
第12步 - 创建一个视图... 11
总结... 13
第2部分 - 开发和配置应用程序.... 14
第13步 – 改进index.jsp. 14
第14步 – 改进视图和控制器... 14
第15步 – 解耦视图和控制器... 16
第16步 – 添加一些业务逻辑的类... 18
第 17 步 – 修改视图用于现实业务数据并且添加消息绑定的支持... 20
第18步 – 添加一些测试数据来自动组装一些业务对象... 20
第19步 – 添加消息绑定以及给build.xml添加“clean”目标... 21
第3部分 - 为应用程序添加单元测试和表单.... 23
第20步 – 为SpringappController添加单元测试... 23
第21步 – 为ProductManager添加单元测试和新的功能... 25
第22步 – 添加一个表单... 27
第4部分 - 实现数据库持久.... 35
第23步 – 添加Ant任务来创建和载入测试数据... 35
第24步 – 为JDBC创建一个数据访问对象(DAO)的实现... 38
第25步 – 修改Web应用来使用数据库持久... 42
第26步 – 修复损坏的测试... 45
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
Example :: Spring Application
只是为了Web应用的完整性,我在war目录中的WEB-INF目录中创建了一个web.xml。
springapp/war/WEB-INF/web.xml
3步 – 将应用程序部署到Tomcat
下面,我要写一个Ant构建脚本,贯穿这个文档我们都要使用它。一个独立的构建脚本包含了应用服务器特定的任务。同样还有用于控制Tomcat下的任务。
springapp/build.xml
这个脚本现在包含了所有我们需要的目标,以便使我们开发更加容易。这里我不会详细解释这个脚本,因为大部分内容都是比较标准Ant和Tomcat的东西。你可以直接复制上面的构建文件并且把它放在你的开发目录的根目录中。我们还需要一个build.properties文件,你需要自定这个文件来配合你的服务器安装。这个文件和build.xml文件在同一个目录中。
springapp/build.properties
# Ant properties for building the springapp appserver.home=${user.home}/jakarta-tomcat-5.0.28 deploy.path=${appserver.home}/webapps tomcat.manager.url=http://localhost:8080/manager tomcat.manager.username=admin tomcat.manager.password=tomcat
如果你是在一个你不是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”,可以从www.springframework.org/download.html 下载到。我把文件解压缩到我的主目录中。我们后面将要用到里面的一些文件。
到此为止必要的环境安装已经完成了,现在我们要开始实际开发我们的Spring Framework MVC应用了。
进入“springapp/war/ WEB-INF”目录。修改我们前面创建的最小“web.xml”文件。现在我们要修改它来满足我们需求。我们定义一个将来控制我们所有请求转向的DispatcherServlet,它将根据我们以后某处输入的信息进行工作。同时还有一个标准的用来映射到我们使用的URL模式的servlet-mapping条目。我决定让所有带“.htm”扩展名的URL转向到“springapp” 分配器。
springapp/war/WEB-INF/web.xml
springapp org.springframework.web.servlet.DispatcherServlet 1 springapp *.htm index.jsp
下面,在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
springappController
首先在“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
import org.springframework.web.servlet.mvc.Controller; import org.springframework.web.servlet.ModelAndView; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class SpringappController implements Controller { public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { return new ModelAndView(""); } }
这是非常基本的控制器。我们稍后会对他进行扩充,同时过会儿我们还要扩展一些已经提供的抽象的基本实现。这个控制器处理请求并返回一个ModelAndView。不过我们还没有定义任何视图,所以现在没什么可做的了。
运行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
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 $JBOSS_HOME/server/default/deploy/log4j.xml! # For all other servers: Comment out the Log4J listener in web.xml to activate Log4J. log4j.rootLogger=INFO, stdout, logfile log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n log4j.appender.logfile=org.apache.log4j.RollingFileAppender log4j.appender.logfile.File=/Users/trisberg/jakarta-tomcat-5.0.28/logs/springapp.log log4j.appender.logfile.MaxFileSize=512KB # Keep three backup files. log4j.appender.logfile.MaxBackupIndex=3 # Pattern to output: date priority [category] - message log4j.appender.logfile.layout=org.apache.log4j.PatternLayout log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n
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
Example :: Spring Application
里面没什么奇特的东西,只是为了现在试一下。下面我们要修改SpringappController来引导到这个视图。
springapp/src/SpringappController.java
import org.springframework.web.servlet.mvc.Controller; import org.springframework.web.servlet.ModelAndView; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class SpringappController implements Controller { /** Logger for this class and subclasses */ protected final Log logger = LogFactory.getLog(getClass()); public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { logger.info("SpringappController - returning hello view"); return new ModelAndView("hello.jsp"); } }
当我在修改这个类的同时,我还添加了一个logger这样我们可以校对我们在这里实际得到的值。更改的内容将以红色标明。这个类返回的模型将最终通过一个ViewResolver来进行转换。由于我们并没有指定一个特别的,所以我们将使用一个默认的,它仅仅引导到匹配指定的视图的名字的URL。我们稍候将修改它。
现在编译并部署这个应用程序。在通知Tomcat重新启动应用程序之后,所有的东西都应该被重新载入了。
让我们在浏览器中试一下——输入URLhttp://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.
现在我们要改进这些部件来建立一个更好的应用程序。
我们将利用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
<%@ page session="false"%> <%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jstl/fmt" %>
现在我们可以更改index.jsp来使用,由于我们使用了JSTL,我们可以使用标签来转向到我们的控制器。
springapp/war/index.jsp
<%@ include file="/WEB-INF/jsp/include.jsp" %> <%-- Redirected because we can‘t set the welcome page to a virtual URL. --%>
现在我要把hello.jsp视图移入WEB-INF/jsp 目录。Index.jsp里面添加的包含文件include.jsp同样也添加到了hello.jsp中。我也使用JSTL标签来输出从传给视图的模型里获取的当前的日期和时间。
springapp/war/WEB-INF/jsp/hello.jsp
<%@ include file="/WEB-INF/jsp/include.jsp" %>Hello :: Spring Application
对于SpringappController.java,我们还要做一些更改。由于我们把文件移动到了一个新的位置,所以需要把视图变成WEB-INF/jsp/hello.jsp。同时添加一个包含当前时间和日期的字符串作为模型。
springapp/src/SpringappController.java
import org.springframework.web.servlet.mvc.Controller; import org.springframework.web.servlet.ModelAndView; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class SpringappController implements Controller { /** Logger for this class and subclasses */ protected final Log logger = LogFactory.getLog(getClass()); public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 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又会调用控制器并把时间和日期发送给视图。
现在控制器是指明了视图的完整路径,这在控制器和视图之间产生了一个多余的依赖关系。理想上来说,我们要使用一个逻辑名称来映射到视图,这可以让我们无需更改控制器就可以切换视图。你可以在一个属性文件中设置这个映射,如果你喜欢使用ResourceBundleViewResolver和SimpleUrlHandlerMapping类的话。如果你的映射需求确实很简单,那么在InternalResourceViewResolver上加上前缀和后缀会很方便。后一种方法就是我现在要实现的。我修改了springapp-servlet.xml并包含了viewResolver条目。我选择使用JstlView,它可以让我们使用JSTL,可以结合消息资源绑定,同时他还可以支持国际化。
springapp/war/WEB-INF/springapp-servlet.xml
springappController org.springframework.web.servlet.view.JstlView /WEB-INF/jsp/ .jsp
所以现在我可以从控制器的视图名称中删除前缀和后缀了。
springapp/src/SpringappController.java
import org.springframework.web.servlet.mvc.Controller; import org.springframework.web.servlet.ModelAndView; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class SpringappController implements Controller { /** Logger for this class and subclasses */ protected final Log logger = LogFactory.getLog(getClass()); public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String now = (new java.util.Date()).toString(); logger.info("returning hello view with " + now); return new ModelAndView("hello", "now", now); } }
编译并部署,应用程序应该仍然可以正常运行。
目前位置我们的应用还不是很有用。我想添加一些业务逻辑,一个Product类和一个管理所有产品的类。我把管理类命名为ProductManager。为了能分离依赖Web的逻辑和业务逻辑,我将在Java源代码重创建两个单独的包——web和bus。如果这个应用程序是为一个真实的公司开发的,我可能会把包命名成像com.mycompany.web和com.mycompany.bus之类的名字,不过这只是一个演示而已我就让包的名称简短一些。Product类是实现为一个JavaBean——它有一个默认的构造器(如果我们没有指明任何构造器,会自动给出),两个实例变量description和price的获取器(getter)和设制器(setter)。我还把它设为Serializable,这对我们的应用不是必需的,不过以后我们很可能要把这个类在不同的应用层中传递的时候,那时就可以直接使用了。
springapp/src/bus/Product.java
package bus; import java.io.Serializable; public class Product implements Serializable { private String description; private Double price; public void setDescription(String s) { description = s; } public String getDescription() { return description; } public void setPrice(Double d) { price = d; } public Double getPrice() { return price; } }
ProductManager中有一个Product的列表List,同样的,这个类也是实现为一个JavaBean。
springapp/src/bus/ProductManager.java
package bus; import java.io.Serializable; import java.util.List; public class ProductManager implements Serializable { private List products; public void setProducts(List p) { products = p; } public List getProducts() { return products; } }
下面,我修改了SpringappController来存放一个指向ProductManager类的引用。正如你所见,它现在在一个单独的web的包中——记得把代码放到这个新位置中。我还要添加让控制器将产品信息传送到视图的代码。getModelAndView现在返回一个Map,同时包含了时间日期和产品管理的引用。
springapp/src/web/SpringappController.java
package web; import org.springframework.web.servlet.mvc.Controller; import org.springframework.web.servlet.ModelAndView; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Map; import java.util.HashMap; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import bus.Product; import bus.ProductManager; public class SpringappController implements Controller { /** Logger for this class and subclasses */ protected final Log logger = LogFactory.getLog(getClass()); private ProductManager prodMan; public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String now = (new java.util.Date()).toString(); logger.info("returning hello view with " + now); 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; } }
我使用了JSTL标签来添加了一个现实产品信息的部分。我还用JSTL标记替换了标题和欢迎文本,这样可以从给定的“message”源中读取文本并显示——在后面的步骤中我会显示这个方法。
springapp/war/WEB-INF/jsp/hello.jsp
<%@ include file="/WEB-INF/jsp/include.jsp" %>
$
我不会添加任何用于从数据库中载入业务对象的代码。然和,我们可以使用Spring的bean和应用程序上下文的支持来牵线到实例的引用。我只要简单地把握需要的数据作为bean之间的偶合条目写入springapp-servlet.xml。我还要添加messageSource条目来引入消息资源绑定(“messages.properties”),在下一步我将创建它。
springapp/war/WEB-INF/springapp-servlet.xml
Lamp 5.75 Table 75.25 Chair 22.79 messages springappController org.springframework.web.servlet.view.JstlView /WEB-INF/jsp/ .jsp
我在war/WEB-INF/classes目录中创建了一个“messages.properties”文件。这个属性绑定文件目前有3个条目可以匹配在标记中指定的键。
springapp/war/WEB-INF/classes/messages.properties
title=SpringApp heading=Hello :: SpringApp greeting=Greetings, it is now
由于我们移动了一些源代码,所以给构建脚本中添加一个“clean”和一个“undeploy”目标。我把以下内容添加到build.xml文件。
现在停止Tomcat服务器,运行clean、undeploy和deploy目标。这应该会删除所有的旧文件,重新构建应用并部署它:
3部分 -为应用程序添加单元测试和表单
SpringappController添加单元测试
在我们创建单元测试之前,我们要先准备Ant并让我们的构建脚本可以处理单元测试。Ant有一个内置的JUnit目标,但是我们需要把junit.jar放入Ant的lib目录中。我使用了Spring分发包中自带的spring-framework-1.2/lib/junit/junit.jar。只要把它复制到你的Ant安装目录的lib目录下即可。我还将以下目标添加到我们构建脚本中:
tests.failed=${tests.failed} *********************************************************** *********************************************************** **** One or more tests failed! Check the output ... **** *********************************************************** ***********************************************************
现在我在src目录中添加了一个新的子目录叫做tests。相信大家也都猜到了,这个目录将包含所有的单元测试。
这些工作结束之后,我们准备开始写我们的第一个单元测试。SpringappController要依赖于HttpServletRequest和HttpServletResponse以及我们的应用程序上下文。由于控制器并没有使用请求和响应,我们直接传送null。如果不是这样,我们要使用EasyMock创建一些模仿对象mock object,这样就可以在测试用使用了。使用某个类,可以在Web Server环境之外载入应用程序上下文。有好几个类可以使用,针对当前的任务我们将使用FileSystemXmlApplicationContext。
springapp/src/tests/TestSpringappController.java
package tests; import java.util.Map; import java.util.List; import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.ServletException; import junit.framework.TestCase; import org.springframework.context.ApplicationContext; import org.springframework.context.support.FileSystemXmlApplicationContext; import org.springframework.web.servlet.ModelAndView; import web.SpringappController; import bus.ProductManager; import bus.Product; public class TestSpringappController extends TestCase { private ApplicationContext ac; public void setUp() throws IOException { ac = new FileSystemXmlApplicationContext("src/tests/WEB-INF/springapp-servlet.xml"); } public void testHandleRequest() throws ServletException, IOException { SpringappController sc = (SpringappController) ac.getBean("springappController"); ModelAndView mav = sc.handleRequest((HttpServletRequest) null, (HttpServletResponse) null); Map m = mav.getModel(); List pl = (List) ((Map) m.get("model")).get("products"); Product p1 = (Product) pl.get(0); assertEquals("Lamp", p1.getDescription()); Product p2 = (Product) pl.get(1); assertEquals("Table", p2.getDescription()); Product p3 = (Product) pl.get(2); assertEquals("Chair", p3.getDescription()); } }
唯一的测试就是调用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
Lamp 5.75 Table 75.25 Chair 22.79
当你运行这个测试的时候,你应该看到载入应用程序上下文时有很多日志信息。
接下来我为ProductManager添加一个测试案例,同时我打算给ProductManager添加一个用于增加价格的新方法,并添加一个测试。
springapp/src/tests/TestProductManager .java
package tests; import java.util.List; import java.util.ArrayList; import junit.framework.TestCase; import bus.ProductManager; import bus.Product; public class TestProductManager extends TestCase { private ProductManager pm; public void setUp() { pm = new ProductManager(); Product p = new Product(); p.setDescription("Chair"); p.setPrice(new Double("20.50")); ArrayList al = new ArrayList(); al.add(p); p = new Product(); p.setDescription("Table"); p.setPrice(new Double("150.10")); al.add(p); pm.setProducts(al); } public void testGetProducs() { List l = pm.getProducts(); Product p1 = (Product) l.get(0); assertEquals("Chair", p1.getDescription()); Product p2 = (Product) l.get(1); assertEquals("Table", p2.getDescription()); } public void testIncreasePrice() { pm.increasePrice(10); List l = pm.getProducts(); Product p = (Product) l.get(0); assertEquals(new Double("22.55"), p.getPrice()); p = (Product) l.get(1); assertEquals(new Double("165.11"), p.getPrice()); } }
对于这个测试,没有必要创建一个应用程序上下文。我只在setUp方法中建立了产品信息并且把他们添加到了产品管理对象中。我还给getProducts和increasePrice添加了测试。increasePrice方法根据传给它的百分比对价格进行增加。我修改了ProductManager类来实现这个新方法。
springapp/src/bus/ProductManager.java
package bus; import java.io.Serializable; import java.util.ListIterator; import java.util.List; public class ProductManager implements Serializable { private List products; public void setProducts(List p) { products = p; } public List getProducts() { return products; } 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类,所以这些类测试起来很方便。
为了在Web应用中提供了一个接口,我添加了一个可以让用户输入百分比值的表单。这个表单使用了一个叫做“spring”的标签库,它是由Spring Framework所提供的。我们要从Spring的分发包中把spring-framework-1.2/dist/spring.tld复制到springapp/war/WEB-INF 目录中。现在我们还要给web.xml添加一个条目。
springapp/war/WEB-INF/web.xml
springapp org.springframework.web.servlet.DispatcherServlet 1 springapp *.htm index.jsp /spring /WEB-INF/spring.tld
我们还需要在jsp文件的page指令中申明这个taglib。我们用普通的方法通过 ">Home
标记是用于将一个表单元素绑定到一个命令对象PriceIncrease.java上的。这个命令对象以后会被传送给效验器,同时如果它通过了检验,它会被继续传送给控制器。${status.errorMessage}和${status.value}是由框架声明的特殊变量,可以用来显示错误信息和当前域的值。
springapp/src/bus/PriceIncrease.java
package bus; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class PriceIncrease { /** Logger for this class and subclasses */ protected final Log logger = LogFactory.getLog(getClass()); private int percentage; public void setPercentage(int i) { percentage = i; logger.info("Percentage set to " + i); } public int getPercentage() { return percentage; } }
这是一个十分简单的JavaBean类,同时这里有一个属性以及他的获取器和设置器。在用户按下了提交按钮之后,Validator类将获取控制。在表单中输入的值会被框架设置在命令对象上。然后会调用方法validate,并传入命令对象和一个用来存放错误信息的对象。
springapp/src/bus/PriceIncreaseValidator.java
package bus; import java.io.Serializable; import org.springframework.validation.Validator; import org.springframework.validation.Errors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class PriceIncreaseValidator implements Validator { private int DEFAULT_MIN_PERCENTAGE = 0; private int DEFAULT_MAX_PERCENTAGE = 50; private int minPercentage = DEFAULT_MIN_PERCENTAGE; private int maxPercentage = DEFAULT_MAX_PERCENTAGE; /** Logger for this class and subclasses */ protected final Log logger = LogFactory.getLog(getClass()); public boolean supports(Class clazz) { return clazz.equals(PriceIncrease.class); } public void validate(Object obj, Errors errors) { PriceIncrease pi = (PriceIncrease) obj; if (pi == null) { errors.rejectValue("percentage", "error.not-specified", null, "Value required."); } else { logger.info("Validating with " + pi + ": " + pi.getPercentage()); if (pi.getPercentage() > maxPercentage) { errors.rejectValue("percentage", "error.too-high", new Object[] {new Integer(maxPercentage)}, "Value too high."); } if (pi.getPercentage() <= minPercentage) { errors.rejectValue("percentage", "error.too-low", new Object[] {new Integer(minPercentage)}, "Value too low."); } } } public void setMinPercentage(int i) { minPercentage = i; } public int getMinPercentage() { return minPercentage; } public void setMaxPercentage(int i) { maxPercentage = i; } public int getMaxPercentage() { return maxPercentage; } }
现在我们要在springapp-servlet.xml文件中添加一条内容来定义新的表单和控制器。我们定义命令对象和效验器的属性。我们还要指明两个视图,一个用来显示表单,另一个将是在成功的表单处理之后我们将看到的。后一个也叫做成功视图,可以是两种类型之一:它可以是一个普通的视图引用直接引导到我们某个JSP页面。但这种方法的一个缺点是,如果用户刷新页面,那么表单的数据就会被重新提交,然后你可能最后就做了两次priceIncreace。另一种方法是使用一个重定向,它将给用户浏览器返回一个应答并且指示浏览器重定向到一个新的URL。我们这里使用的这个URL不可以是我们的JSP页面之一,因为他们对于直接访问是不可见的。必须一个从外部可以获取的URL。所以我选择了“hello.htm”来作为我的重定向URL。这个URL影射到“hello.jsp”页面,这个应该运行得很令人满意。
springapp/war/WEB-INF/springapp-servlet.xml
true priceIncrease bus.PriceIncrease priceincrease hello.htm Lamp 5.75 Table 75.25 Chair 22.79 messages springappController priceIncreaseForm org.springframework.web.servlet.view.JstlView /WEB-INF/jsp/ .jsp
下面,让我们看一下这个表单的控制器。onSubmit方法获取了控制并且在它调用ProductManager对象的increasePrice方法之前进行了一些日志记录。然后它使用successView的url创建了RedirectView的一个新的实例,并传递这个实例给ModelAndView,最后返回这个ModelAndView的实例。
springapp/src/web/PriceIncreaseFormController.java
package web; import org.springframework.web.servlet.mvc.SimpleFormController; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.view.RedirectView; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Map; import java.util.HashMap; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import bus.Product; import bus.ProductManager; import bus.PriceIncrease; public class PriceIncreaseFormController extends SimpleFormController { /** Logger for this class and subclasses */ protected final Log logger = LogFactory.getLog(getClass()); private ProductManager prodMan; public ModelAndView onSubmit(Object command) throws ServletException { int increase = ((PriceIncrease) command).getPercentage(); logger.info("Increasing prices by " + increase + "%."); prodMan.increasePrice(increase); String now = (new java.util.Date()).toString(); logger.info("returning from PriceIncreaseForm view to " + getSuccessView() + " with " + now); Map myModel = new HashMap(); myModel.put("now", now); myModel.put("products", getProductManager().getProducts()); return new ModelAndView(new RedirectView(getSuccessView())); } protected Object formBackingObject(HttpServletRequest request) throws ServletException { PriceIncrease priceIncrease = new PriceIncrease(); priceIncrease.setPercentage(20); return priceIncrease; } public void setProductManager(ProductManager pm) { prodMan = pm; } public ProductManager getProductManager() { return prodMan; } }
我们还要在message.properties资源文件里面添加一些消息。
springapp/war/WEB-INF/classes/messages.properties
title=SpringApp heading=Hello :: SpringApp greeting=Greetings, it is now 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页面的链接。
springapp/war/WEB-INF/jsp/hello.jsp
<%@ include file="/WEB-INF/jsp/include.jsp" %>
Spring Framework MVC
应用程序
作者:Thomas Risberg
2003年7月(2005年4月修订)
翻译 Shining Ray @Nirvana Studio
这是一个关于如何使用Spring Framework从无到有开发一个Web应用的逐步的指南。本文分成几个部分。你可以按顺序阅读或者根据你对他们的熟悉程度,跳过某些章节。
目录
第1部分 - 设置基本应用程序和环境.... 2
第1步 - 开发目录... 2
第2步 – index.jsp. 2
第3步 – 将应用程序部署到Tomcat.. 2
第4步 – 测试应用... 6
第5步 – 下载Spring分发包... 7
第6步 – 修改WEB-INF目录中的web.xml. 7
第7步 - 把jar文件复制到WEB-INF/lib.. 8
第8步 - 创建你的控制器... 9
第9步 - 构建应用程序... 9
第10步 – 复制并修改log4j.properties. 9
第11步 – 部署应用程序... 10
第12步 - 创建一个视图... 11
总结... 13
第2部分 - 开发和配置应用程序.... 14
第13步 – 改进index.jsp. 14
第14步 – 改进视图和控制器... 14
第15步 – 解耦视图和控制器... 16
第16步 – 添加一些业务逻辑的类... 18
第 17 步 – 修改视图用于现实业务数据并且添加消息绑定的支持... 20
第18步 – 添加一些测试数据来自动组装一些业务对象... 20
第19步 – 添加消息绑定以及给build.xml添加“clean”目标... 21
第3部分 - 为应用程序添加单元测试和表单.... 23
第20步 – 为SpringappController添加单元测试... 23
第21步 – 为ProductManager添加单元测试和新的功能... 25
第22步 – 添加一个表单... 27
第4部分 - 实现数据库持久.... 35
第23步 – 添加Ant任务来创建和载入测试数据... 35
第24步 – 为JDBC创建一个数据访问对象(DAO)的实现... 38
第25步 – 修改Web应用来使用数据库持久... 42
第26步 – 修复损坏的测试... 45
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
Example - Spring Application
This is my test.
只是为了Web应用的完整性,我在war目录中的WEB-INF目录中创建了一个web.xml。
springapp/war/WEB-INF/web.xml
3步 – 将应用程序部署到Tomcat
下面,我要写一个Ant构建脚本,贯穿这个文档我们都要使用它。一个独立的构建脚本包含了应用服务器特定的任务。同样还有用于控制Tomcat下的任务。
springapp/build.xml
这个脚本现在包含了所有我们需要的目标,以便使我们开发更加容易。这里我不会详细解释这个脚本,因为大部分内容都是比较标准Ant和Tomcat的东西。你可以直接复制上面的构建文件并且把它放在你的开发目录的根目录中。我们还需要一个build.properties文件,你需要自定这个文件来配合你的服务器安装。这个文件和build.xml文件在同一个目录中。
springapp/build.properties
# Ant properties for building the springapp appserver.home=${user.home}/jakarta-tomcat-5.0.28 deploy.path=${appserver.home}/webapps tomcat.manager.url=http://localhost:8080/manager tomcat.manager.username=admin tomcat.manager.password=tomcat
如果你是在一个你不是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”,可以从www.springframework.org/download.html 下载到。我把文件解压缩到我的主目录中。我们后面将要用到里面的一些文件。
到此为止必要的环境安装已经完成了,现在我们要开始实际开发我们的Spring Framework MVC应用了。
进入“springapp/war/ WEB-INF”目录。修改我们前面创建的最小“web.xml”文件。现在我们要修改它来满足我们需求。我们定义一个将来控制我们所有请求转向的DispatcherServlet,它将根据我们以后某处输入的信息进行工作。同时还有一个标准的用来映射到我们使用的URL模式的servlet-mapping条目。我决定让所有带“.htm”扩展名的URL转向到“springapp” 分配器。
springapp/war/WEB-INF/web.xml
下面,在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
首先在“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
import org.springframework.web.servlet.mvc.Controller; import org.springframework.web.servlet.ModelAndView; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class SpringappController implements Controller { public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { return new ModelAndView(""); } }
这是非常基本的控制器。我们稍后会对他进行扩充,同时过会儿我们还要扩展一些已经提供的抽象的基本实现。这个控制器处理请求并返回一个ModelAndView。不过我们还没有定义任何视图,所以现在没什么可做的了。
运行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
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 $JBOSS_HOME/server/default/deploy/log4j.xml! # For all other servers: Comment out the Log4J listener in web.xml to activate Log4J. log4j.rootLogger=INFO, stdout, logfile log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n log4j.appender.logfile=org.apache.log4j.RollingFileAppender log4j.appender.logfile.File=/Users/trisberg/jakarta-tomcat-5.0.28/logs/springapp.log log4j.appender.logfile.MaxFileSize=512KB # Keep three backup files. log4j.appender.logfile.MaxBackupIndex=3 # Pattern to output: date priority [category] - message log4j.appender.logfile.layout=org.apache.log4j.PatternLayout log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n
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
Hello - Spring Application
Greetings.
里面没什么奇特的东西,只是为了现在试一下。下面我们要修改SpringappController来引导到这个视图。
springapp/src/SpringappController.java
import org.springframework.web.servlet.mvc.Controller; import org.springframework.web.servlet.ModelAndView; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class SpringappController implements Controller { /** Logger for this class and subclasses */ protected final Log logger = LogFactory.getLog(getClass()); public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { logger.info("SpringappController - returning hello view"); return new ModelAndView("hello.jsp"); } }
当我在修改这个类的同时,我还添加了一个logger这样我们可以校对我们在这里实际得到的值。更改的内容将以红色标明。这个类返回的模型将最终通过一个ViewResolver来进行转换。由于我们并没有指定一个特别的,所以我们将使用一个默认的,它仅仅引导到匹配指定的视图的名字的URL。我们稍候将修改它。
现在编译并部署这个应用程序。在通知Tomcat重新启动应用程序之后,所有的东西都应该被重新载入了。
让我们在浏览器中试一下——输入URLhttp://localhost:8080/springapp/hello.htm,然后我们应该看到以下内容:
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.
现在我们要改进这些部件来建立一个更好的应用程序。
我们将利用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
<%@ page session="false"%> <%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jstl/fmt" %>
现在我们可以更改index.jsp来使用,由于我们使用了JSTL,我们可以使用
springapp/war/index.jsp
<%@ include file="/WEB-INF/jsp/include.jsp" %> <%-- Redirected because we can‘t set the welcome page to a virtual URL. --%>
现在我要把hello.jsp视图移入WEB-INF/jsp 目录。Index.jsp里面添加的包含文件include.jsp同样也添加到了hello.jsp中。我也使用JSTL
springapp/war/WEB-INF/jsp/hello.jsp
<%@ include file="/WEB-INF/jsp/include.jsp" %>
Hello - Spring Application
Greetings, it is now
对于SpringappController.java,我们还要做一些更改。由于我们把文件移动到了一个新的位置,所以需要把视图变成WEB-INF/jsp/hello.jsp。同时添加一个包含当前时间和日期的字符串作为模型。
springapp/src/SpringappController.java
import org.springframework.web.servlet.mvc.Controller; import org.springframework.web.servlet.ModelAndView; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class SpringappController implements Controller { /** Logger for this class and subclasses */ protected final Log logger = LogFactory.getLog(getClass()); public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 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又会调用控制器并把时间和日期发送给视图。
现在控制器是指明了视图的完整路径,这在控制器和视图之间产生了一个多余的依赖关系。理想上来说,我们要使用一个逻辑名称来映射到视图,这可以让我们无需更改控制器就可以切换视图。你可以在一个属性文件中设置这个映射,如果你喜欢使用ResourceBundleViewResolver和SimpleUrlHandlerMapping类的话。如果你的映射需求确实很简单,那么在InternalResourceViewResolver上加上前缀和后缀会很方便。后一种方法就是我现在要实现的。我修改了springapp-servlet.xml并包含了viewResolver条目。我选择使用JstlView,它可以让我们使用JSTL,可以结合消息资源绑定,同时他还可以支持国际化。
springapp/war/WEB-INF/springapp-servlet.xml
所以现在我可以从控制器的视图名称中删除前缀和后缀了。
springapp/src/SpringappController.java
import org.springframework.web.servlet.mvc.Controller; import org.springframework.web.servlet.ModelAndView; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class SpringappController implements Controller { /** Logger for this class and subclasses */ protected final Log logger = LogFactory.getLog(getClass()); public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String now = (new java.util.Date()).toString(); logger.info("returning hello view with " + now); return new ModelAndView("hello", "now", now); } }
编译并部署,应用程序应该仍然可以正常运行。
目前位置我们的应用还不是很有用。我想添加一些业务逻辑,一个Product类和一个管理所有产品的类。我把管理类命名为ProductManager。为了能分离依赖Web的逻辑和业务逻辑,我将在Java源代码重创建两个单独的包——web和bus。如果这个应用程序是为一个真实的公司开发的,我可能会把包命名成像com.mycompany.web和com.mycompany.bus之类的名字,不过这只是一个演示而已我就让包的名称简短一些。Product类是实现为一个JavaBean——它有一个默认的构造器(如果我们没有指明任何构造器,会自动给出),两个实例变量description和price的获取器(getter)和设制器(setter)。我还把它设为Serializable,这对我们的应用不是必需的,不过以后我们很可能要把这个类在不同的应用层中传递的时候,那时就可以直接使用了。
springapp/src/bus/Product.java
package bus; import java.io.Serializable; public class Product implements Serializable { private String description; private Double price; public void setDescription(String s) { description = s; } public String getDescription() { return description; } public void setPrice(Double d) { price = d; } public Double getPrice() { return price; } }
ProductManager中有一个Product的列表List,同样的,这个类也是实现为一个JavaBean。
springapp/src/bus/ProductManager.java
package bus; import java.io.Serializable; import java.util.List; public class ProductManager implements Serializable { private List products; public void setProducts(List p) { products = p; } public List getProducts() { return products; } }
下面,我修改了SpringappController来存放一个指向ProductManager类的引用。正如你所见,它现在在一个单独的web的包中——记得把代码放到这个新位置中。我还要添加让控制器将产品信息传送到视图的代码。getModelAndView现在返回一个Map,同时包含了时间日期和产品管理的引用。
springapp/src/web/SpringappController.java
package web; import org.springframework.web.servlet.mvc.Controller; import org.springframework.web.servlet.ModelAndView; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Map; import java.util.HashMap; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import bus.Product; import bus.ProductManager; public class SpringappController implements Controller { /** Logger for this class and subclasses */ protected final Log logger = LogFactory.getLog(getClass()); private ProductManager prodMan; public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String now = (new java.util.Date()).toString(); logger.info("returning hello view with " + now); 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; } }
我使用了JSTL
springapp/war/WEB-INF/jsp/hello.jsp
<%@ include file="/WEB-INF/jsp/include.jsp" %>
Products
我不会添加任何用于从数据库中载入业务对象的代码。然和,我们可以使用Spring的bean和应用程序上下文的支持来牵线到实例的引用。我只要简单地把握需要的数据作为bean之间的偶合条目写入springapp-servlet.xml。我还要添加messageSource条目来引入消息资源绑定(“messages.properties”),在下一步我将创建它。
springapp/war/WEB-INF/springapp-servlet.xml
我在war/WEB-INF/classes目录中创建了一个“messages.properties”文件。这个属性绑定文件目前有3个条目可以匹配在
springapp/war/WEB-INF/classes/messages.properties
title=SpringApp heading=Hello :: SpringApp greeting=Greetings, it is now
由于我们移动了一些源代码,所以给构建脚本中添加一个“clean”和一个“undeploy”目标。我把以下内容添加到build.xml文件。
现在停止Tomcat服务器,运行clean、undeploy和deploy目标。这应该会删除所有的旧文件,重新构建应用并部署它:
3部分 -为应用程序添加单元测试和表单
SpringappController添加单元测试
在我们创建单元测试之前,我们要先准备Ant并让我们的构建脚本可以处理单元测试。Ant有一个内置的JUnit目标,但是我们需要把junit.jar放入Ant的lib目录中。我使用了Spring分发包中自带的spring-framework-1.2/lib/junit/junit.jar。只要把它复制到你的Ant安装目录的lib目录下即可。我还将以下目标添加到我们构建脚本中:
现在我在src目录中添加了一个新的子目录叫做tests。相信大家也都猜到了,这个目录将包含所有的单元测试。
这些工作结束之后,我们准备开始写我们的第一个单元测试。SpringappController要依赖于HttpServletRequest和HttpServletResponse以及我们的应用程序上下文。由于控制器并没有使用请求和响应,我们直接传送null。如果不是这样,我们要使用EasyMock创建一些模仿对象mock object,这样就可以在测试用使用了。使用某个类,可以在Web Server环境之外载入应用程序上下文。有好几个类可以使用,针对当前的任务我们将使用FileSystemXmlApplicationContext。
springapp/src/tests/TestSpringappController.java
package tests; import java.util.Map; import java.util.List; import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.ServletException; import junit.framework.TestCase; import org.springframework.context.ApplicationContext; import org.springframework.context.support.FileSystemXmlApplicationContext; import org.springframework.web.servlet.ModelAndView; import web.SpringappController; import bus.ProductManager; import bus.Product; public class TestSpringappController extends TestCase { private ApplicationContext ac; public void setUp() throws IOException { ac = new FileSystemXmlApplicationContext("src/tests/WEB-INF/springapp-servlet.xml"); } public void testHandleRequest() throws ServletException, IOException { SpringappController sc = (SpringappController) ac.getBean("springappController"); ModelAndView mav = sc.handleRequest((HttpServletRequest) null, (HttpServletResponse) null); Map m = mav.getModel(); List pl = (List) ((Map) m.get("model")).get("products"); Product p1 = (Product) pl.get(0); assertEquals("Lamp", p1.getDescription()); Product p2 = (Product) pl.get(1); assertEquals("Table", p2.getDescription()); Product p3 = (Product) pl.get(2); assertEquals("Chair", p3.getDescription()); } }
唯一的测试就是调用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
当你运行这个测试的时候,你应该看到载入应用程序上下文时有很多日志信息。
接下来我为ProductManager添加一个测试案例,同时我打算给ProductManager添加一个用于增加价格的新方法,并添加一个测试。
springapp/src/tests/TestProductManager .java
package tests; import java.util.List; import java.util.ArrayList; import junit.framework.TestCase; import bus.ProductManager; import bus.Product; public class TestProductManager extends TestCase { private ProductManager pm; public void setUp() { pm = new ProductManager(); Product p = new Product(); p.setDescription("Chair"); p.setPrice(new Double("20.50")); ArrayList al = new ArrayList(); al.add(p); p = new Product(); p.setDescription("Table"); p.setPrice(new Double("150.10")); al.add(p); pm.setProducts(al); } public void testGetProducs() { List l = pm.getProducts(); Product p1 = (Product) l.get(0); assertEquals("Chair", p1.getDescription()); Product p2 = (Product) l.get(1); assertEquals("Table", p2.getDescription()); } public void testIncreasePrice() { pm.increasePrice(10); List l = pm.getProducts(); Product p = (Product) l.get(0); assertEquals(new Double("22.55"), p.getPrice()); p = (Product) l.get(1); assertEquals(new Double("165.11"), p.getPrice()); } }
对于这个测试,没有必要创建一个应用程序上下文。我只在setUp方法中建立了产品信息并且把他们添加到了产品管理对象中。我还给getProducts和increasePrice添加了测试。increasePrice方法根据传给它的百分比对价格进行增加。我修改了ProductManager类来实现这个新方法。
springapp/src/bus/ProductManager.java
package bus; import java.io.Serializable; import java.util.ListIterator; import java.util.List; public class ProductManager implements Serializable { private List products; public void setProducts(List p) { products = p; } public List getProducts() { return products; } 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类,所以这些类测试起来很方便。
为了在Web应用中提供了一个接口,我添加了一个可以让用户输入百分比值的表单。这个表单使用了一个叫做“spring”的标签库,它是由Spring Framework所提供的。我们要从Spring的分发包中把spring-framework-1.2/dist/spring.tld复制到springapp/war/WEB-INF 目录中。现在我们还要给web.xml添加一个
springapp/war/WEB-INF/web.xml
我们还需要在jsp文件的page指令中申明这个taglib。我们用普通的方法通过 ">Home
springapp/src/bus/PriceIncrease.java
package bus; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class PriceIncrease { /** Logger for this class and subclasses */ protected final Log logger = LogFactory.getLog(getClass()); private int percentage; public void setPercentage(int i) { percentage = i; logger.info("Percentage set to " + i); } public int getPercentage() { return percentage; } }
这是一个十分简单的JavaBean类,同时这里有一个属性以及他的获取器和设置器。在用户按下了提交按钮之后,Validator类将获取控制。在表单中输入的值会被框架设置在命令对象上。然后会调用方法validate,并传入命令对象和一个用来存放错误信息的对象。
springapp/src/bus/PriceIncreaseValidator.java
package bus; import java.io.Serializable; import org.springframework.validation.Validator; import org.springframework.validation.Errors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class PriceIncreaseValidator implements Validator { private int DEFAULT_MIN_PERCENTAGE = 0; private int DEFAULT_MAX_PERCENTAGE = 50; private int minPercentage = DEFAULT_MIN_PERCENTAGE; private int maxPercentage = DEFAULT_MAX_PERCENTAGE; /** Logger for this class and subclasses */ protected final Log logger = LogFactory.getLog(getClass()); public boolean supports(Class clazz) { return clazz.equals(PriceIncrease.class); } public void validate(Object obj, Errors errors) { PriceIncrease pi = (PriceIncrease) obj; if (pi == null) { errors.rejectValue("percentage", "error.not-specified", null, "Value required."); } else { logger.info("Validating with " + pi + ": " + pi.getPercentage()); if (pi.getPercentage() > maxPercentage) { errors.rejectValue("percentage", "error.too-high", new Object[] {new Integer(maxPercentage)}, "Value too high."); } if (pi.getPercentage() <= minPercentage) { errors.rejectValue("percentage", "error.too-low", new Object[] {new Integer(minPercentage)}, "Value too low."); } } } public void setMinPercentage(int i) { minPercentage = i; } public int getMinPercentage() { return minPercentage; } public void setMaxPercentage(int i) { maxPercentage = i; } public int getMaxPercentage() { return maxPercentage; } }
现在我们要在springapp-servlet.xml文件中添加一条内容来定义新的表单和控制器。我们定义命令对象和效验器的属性。我们还要指明两个视图,一个用来显示表单,另一个将是在成功的表单处理之后我们将看到的。后一个也叫做成功视图,可以是两种类型之一:它可以是一个普通的视图引用直接引导到我们某个JSP页面。但这种方法的一个缺点是,如果用户刷新页面,那么表单的数据就会被重新提交,然后你可能最后就做了两次priceIncreace。另一种方法是使用一个重定向,它将给用户浏览器返回一个应答并且指示浏览器重定向到一个新的URL。我们这里使用的这个URL不可以是我们的JSP页面之一,因为他们对于直接访问是不可见的。必须一个从外部可以获取的URL。所以我选择了“hello.htm”来作为我的重定向URL。这个URL影射到“hello.jsp”页面,这个应该运行得很令人满意。
springapp/war/WEB-INF/springapp-servlet.xml
下面,让我们看一下这个表单的控制器。onSubmit方法获取了控制并且在它调用ProductManager对象的increasePrice方法之前进行了一些日志记录。然后它使用successView的url创建了RedirectView的一个新的实例,并传递这个实例给ModelAndView,最后返回这个ModelAndView的实例。
springapp/src/web/PriceIncreaseFormController.java
package web; import org.springframework.web.servlet.mvc.SimpleFormController; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.view.RedirectView; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Map; import java.util.HashMap; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import bus.Product; import bus.ProductManager; import bus.PriceIncrease; public class PriceIncreaseFormController extends SimpleFormController { /** Logger for this class and subclasses */ protected final Log logger = LogFactory.getLog(getClass()); private ProductManager prodMan; public ModelAndView onSubmit(Object command) throws ServletException { int increase = ((PriceIncrease) command).getPercentage(); logger.info("Increasing prices by " + increase + "%."); prodMan.increasePrice(increase); String now = (new java.util.Date()).toString(); logger.info("returning from PriceIncreaseForm view to " + getSuccessView() + " with " + now); Map myModel = new HashMap(); myModel.put("now", now); myModel.put("products", getProductManager().getProducts()); return new ModelAndView(new RedirectView(getSuccessView())); } protected Object formBackingObject(HttpServletRequest request) throws ServletException { PriceIncrease priceIncrease = new PriceIncrease(); priceIncrease.setPercentage(20); return priceIncrease; } public void setProductManager(ProductManager pm) { prodMan = pm; } public ProductManager getProductManager() { return prodMan; } }
我们还要在message.properties资源文件里面添加一些消息。
springapp/war/WEB-INF/classes/messages.properties
title=SpringApp heading=Hello :: SpringApp greeting=Greetings, it is now 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页面的链接。
springapp/war/WEB-INF/jsp/hello.jsp
<%@ include file="/WEB-INF/jsp/include.jsp" %>