UML软件工程组织

一步一步开发Spring Framework MVC应用程序
作者:Thomas Risberg   翻译 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

20SpringappController添加单元测试... 23

21ProductManager添加单元测试和新的功能... 25

22添加一个表单... 27

4部分 - 实现数据库持久.... 35

23添加Ant任务来创建和载入测试数据... 35

24JDBC创建一个数据访问对象(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

<html>
<head><title>Example :: Spring Application</title></head>
<body>
<h1>Example - Spring Application</h1>
<p>This is my test.</p>
</body>
</html>

只是为了Web应用的完整性,我在war目录中的WEB-INF目录中创建了一个web.xml

springapp/war/WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app PUBLIC '-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN'
'http://java.sun.com/dtd/web-app_2_3.dtd'>
<web-app>
</web-app>

第3步 – 将应用程序部署到Tomcat

下面,我要写一个Ant构建脚本,贯穿这个文档我们都要使用它。一个独立的构建脚本包含了应用服务器特定的任务。同样还有用于控制Tomcat下的任务。

springapp/build.xml

<?xml version="1.0"?>
<project name="springapp" basedir="." default="usage">
    <property file="build.properties"/>
    <property name="src.dir" value="src"/>
    <property name="web.dir" value="war"/>
    <property name="build.dir" value="${web.dir}/WEB-INF/classes"/>
    <property name="name" value="springapp"/>
    <path id="master-classpath">
        <fileset dir="${web.dir}/WEB-INF/lib">
            <include name="*.jar"/>
        </fileset>
        <!-- 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 -->
        <fileset dir="${appserver.home}/common/lib">
            <include name="servlet*.jar"/>
        </fileset>
        <pathelement path="${build.dir}"/>
    </path>
    <target name="usage">
        <echo message=""/>
        <echo message="${name} build file"/>
        <echo message="-----------------------------------"/>
        <echo message=""/>
        <echo message="Available targets are:"/>
        <echo message=""/>
        <echo message="build     --> Build the application"/>
        <echo message="deploy    --> Deploy application as directory"/>
        <echo message="deploywar --> Deploy application as a WAR file"/>
        <echo message="install   --> Install application in Tomcat"/>
        <echo message="reload    --> Reload application in Tomcat"/>
        <echo message="start     --> Start Tomcat application"/>
        <echo message="stop      --> Stop Tomcat application"/>
        <echo message="list      --> List Tomcat applications"/>
        <echo message=""/>
    </target>
    <target name="build" description="Compile main source tree java files">
        <mkdir dir="${build.dir}"/>
        <javac destdir="${build.dir}" target="1.3" debug="true"
               deprecation="false" optimize="false" failonerror="true">
            <src path="${src.dir}"/>
            <classpath refid="master-classpath"/>
        </javac>
    </target>
    <target name="deploy" depends="build" description="Deploy application">
        <copy todir="${deploy.path}/${name}" preservelastmodified="true">
            <fileset dir="${web.dir}">
                <include name="**/*.*"/>
            </fileset>
        </copy>
    </target>
    <target name="deploywar" depends="build" description="Deploy application as a WAR file">
        <war destfile="${name}.war"
             webxml="${web.dir}/WEB-INF/web.xml">
            <fileset dir="${web.dir}">
                <include name="**/*.*"/>
            </fileset>
        </war>
        <copy todir="${deploy.path}" preservelastmodified="true">
            <fileset dir=".">
                <include name="*.war"/>
            </fileset>
        </copy>
    </target>
<!-- ============================================================== -->
<!-- Tomcat tasks - remove these if you don't have Tomcat installed -->
<!-- ============================================================== -->
    <taskdef name="install" classname="org.apache.catalina.ant.InstallTask">
        <classpath>
            <path location="${appserver.home}/server/lib/catalina-ant.jar"/>
        </classpath>
    </taskdef>
    <taskdef name="reload" classname="org.apache.catalina.ant.ReloadTask">
        <classpath>
            <path location="${appserver.home}/server/lib/catalina-ant.jar"/>
        </classpath>
    </taskdef>
    <taskdef name="list" classname="org.apache.catalina.ant.ListTask">
        <classpath>
            <path location="${appserver.home}/server/lib/catalina-ant.jar"/>
        </classpath>
    </taskdef>
    <taskdef name="start" classname="org.apache.catalina.ant.StartTask">
        <classpath>
            <path location="${appserver.home}/server/lib/catalina-ant.jar"/>
        </classpath>
    </taskdef>
    <taskdef name="stop" classname="org.apache.catalina.ant.StopTask">
        <classpath>
            <path location="${appserver.home}/server/lib/catalina-ant.jar"/>
        </classpath>
    </taskdef>
    <target name="install" description="Install application in Tomcat">
        <install url="${tomcat.manager.url}"
                 username="${tomcat.manager.username}"
                 password="${tomcat.manager.password}"
                 path="/${name}"
                 war="${name}"/>
    </target>
    <target name="reload" description="Reload application in Tomcat">
        <reload url="${tomcat.manager.url}"
                 username="${tomcat.manager.username}"
                 password="${tomcat.manager.password}"
                 path="/${name}"/>
    </target>
    <target name="start" description="Start Tomcat application">
        <start url="${tomcat.manager.url}"
                 username="${tomcat.manager.username}"
                 password="${tomcat.manager.password}"
                 path="/${name}"/>
    </target>
    <target name="stop" description="Stop Tomcat application">
        <stop url="${tomcat.manager.url}"
                 username="${tomcat.manager.username}"
                 password="${tomcat.manager.password}"
                 path="/${name}"/>
    </target>
    <target name="list" description="List Tomcat applications">
        <list url="${tomcat.manager.url}"
                 username="${tomcat.manager.username}"
                 password="${tomcat.manager.password}"/>
    </target>
<!-- End Tomcat tasks -->
</project>

这个脚本现在包含了所有我们需要的目标,以便使我们开发更加容易。这里我不会详细解释这个脚本,因为大部分内容都是比较标准AntTomcat的东西。你可以直接复制上面的构建文件并且把它放在你的开发目录的根目录中。我们还需要一个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应用了。

6步 – 修改WEB-INF目录中的web.xml

进入“springapp/war/ WEB-INF目录。修改我们前面创建的最小“web.xml文件。现在我们要修改它来满足我们需求。我们定义一个将来控制我们所有请求转向的DispatcherServlet,它将根据我们以后某处输入的信息进行工作。同时还有一个标准的用来映射到我们使用的URL模式的servlet-mapping条目。我决定让所有带“.htm扩展名的URL转向到“springapp 分配器。

springapp/war/WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app PUBLIC '-//Sun Microsystems, 
Inc.//DTD Web Application 2.3//EN' 'http://java.sun.com/dtd/web-app_2_3.dtd'>
<web-app>
  <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>
</web-app>

下面,在springapp/war/WEB-INF目录下创建一个叫做“springapp-servlet.xml的文件(你可以直接从Spring分发包中复制一个范例文件,位于sample/skeletons/webapp-minimal 目录中)。DispatcherServlet所使用的定义就要放在这个文件中。文件名是web.xml中的servlet-name并加上“-servlet”后缀。这是Spring Framework所使用的标准命名约定。现在,添加一个叫做springappControllerbean条目并创建一个SpringappController类。这里将定义我们的应用程序所使用的控制器。我们还要添加一个URL映射 urlMapping这样DispatcherServlet就会知道对于不同的URL应该调用哪个控制器。

springapp/war/WEB-INF/springapp-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<!--
  - Application context definition for "springapp" DispatcherServlet.
  -->
<beans>
    <bean id="springappController" class="SpringappController"/>
    <bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/hello.htm">springappController</prop>
            </props>
        </property>
    </bean>
</beans>

7 - jar文件复制到WEB-INF/lib

首先在“war/WEB-INF目录中创建一个“lib”目录。然后,从Spring分发包中,将spring.jar(spring-framework-1.2/dist/spring.jar)复制到新建的war/WEB-INF/lib目录中。同时把commons-loggingjar文件(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。不过我们还没有定义任何视图,所以现在没什么可做的了。

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 $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

<html>
<head><title>Example :: Spring Application</title></head>
<body>
<h1>Hello - Spring Application</h1>
<p>Greetings.</p>
</body>
</html>

里面没什么奇特的东西,只是为了现在试一下。下面我们要修改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重新启动应用程序之后,所有的东西都应该被重新载入了。

让我们在浏览器中试一下——输入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.jarspringapp/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,我们可以使用<c:redirect>标签来转向到我们的控制器。

springapp/war/index.jsp

<%@ include file="/WEB-INF/jsp/include.jsp" %>
 
<%-- Redirected because we can't set the welcome page to a virtual URL. --%>
<c:redirect url="/hello.htm"/>

14改进视图和控制器

现在我要把hello.jsp视图移入WEB-INF/jsp 目录。Index.jsp里面添加的包含文件include.jsp同样也添加到了hello.jsp中。我也使用JSTL<c:out>标签来输出从传给视图的模型里获取的当前的日期和时间。

springapp/war/WEB-INF/jsp/hello.jsp

<%@ include file="/WEB-INF/jsp/include.jsp" %>
 
<html>
<head><title>Hello :: Spring Application</title></head>
<body>
<h1>Hello - Spring Application</h1>
<p>Greetings, it is now <c:out value="${now}"/>
</p>
</body>
</html>

对于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又会调用控制器并把时间和日期发送给视图。

15解耦视图和控制器

现在控制器是指明了视图的完整路径,这在控制器和视图之间产生了一个多余的依赖关系。理想上来说,我们要使用一个逻辑名称来映射到视图,这可以让我们无需更改控制器就可以切换视图。你可以在一个属性文件中设置这个映射,如果你喜欢使用ResourceBundleViewResolverSimpleUrlHandlerMapping类的话。如果你的映射需求确实很简单,那么在InternalResourceViewResolver上加上前缀和后缀会很方便。后一种方法就是我现在要实现的。我修改了springapp-servlet.xml并包含了viewResolver条目。我选择使用JstlView,它可以让我们使用JSTL,可以结合消息资源绑定,同时他还可以支持国际化。

springapp/war/WEB-INF/springapp-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
 
<!--
  - Application context definition for "springapp" DispatcherServlet.
  -->
 
<beans>
    <bean id="springappController" class="SpringappController"/>
    <bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
 
        <property name="mappings">
            <props>
                <prop key="/hello.htm">springappController</prop>
            </props>
        </property>
    </bean>
 
      <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>
</beans>        

所以现在我可以从控制器的视图名称中删除前缀和后缀了。

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);
    }
}

编译并部署,应用程序应该仍然可以正常运行。

16添加一些业务逻辑的类

目前位置我们的应用还不是很有用。我想添加一些业务逻辑,一个Product类和一个管理所有产品的类。我把管理类命名为ProductManager。为了能分离依赖Web的逻辑和业务逻辑,我将在Java源代码重创建两个单独的包——webbus。如果这个应用程序是为一个真实的公司开发的,我可能会把包命名成像com.mycompany.webcom.mycompany.bus之类的名字,不过这只是一个演示而已我就让包的名称简短一些。Product类是实现为一个JavaBean——它有一个默认的构造器(如果我们没有指明任何构造器,会自动给出),两个实例变量descriptionprice的获取器(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;
    }
}

17 修改视图用于现实业务数据并且添加消息绑定的支持

我使用了JSTL<c:forEach>标签来添加了一个现实产品信息的部分。我还用JSTL<fmt:message>标记替换了标题和欢迎文本,这样可以从给定的“message”源中读取文本并显示——在后面的步骤中我会显示这个方法。

springapp/war/WEB-INF/jsp/hello.jsp

<%@ include file="/WEB-INF/jsp/include.jsp" %>
 
 
<html>
<head><title><fmt:message key="title"/></title></head>
<body>
<h1><fmt:message key="heading"/></h1>
<p><fmt:message key="greeting"/> <c:out value="${model.now}"/>
</p>
 
<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>
</body>
</html>

18添加一些测试数据来自动组装一些业务对象

我不会添加任何用于从数据库中载入业务对象的代码。然和,我们可以使用Springbean和应用程序上下文的支持来牵线到实例的引用。我只要简单地把握需要的数据作为bean之间的偶合条目写入springapp-servlet.xml。我还要添加messageSource条目来引入消息资源绑定(“messages.properties”),在下一步我将创建它。

springapp/war/WEB-INF/springapp-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
 
<!--
  - Application context definition for "springapp" DispatcherServlet.
  -->
 
 
<beans>
    <bean id="springappController" class="web.SpringappController">
        <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>
 
    <bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/hello.htm">springappController</prop>
            </props>
        </property>
    </bean>
 
    <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>
</beans>        

19添加消息绑定以及给build.xml添加“clean”目标

我在war/WEB-INF/classes目录中创建了一个“messages.properties”文件。这个属性绑定文件目前有3个条目可以匹配在<fmt:message>标记中指定的键。

springapp/war/WEB-INF/classes/messages.properties

title=SpringApp
 
heading=Hello :: SpringApp
 
greeting=Greetings, it is now

由于我们移动了一些源代码,所以给构建脚本中添加一个“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服务器,运行cleanundeploydeploy目标。这应该会删除所有的旧文件,重新构建应用并部署它:

3部分 -为应用程序添加单元测试和表单

20SpringappController添加单元测试

在我们创建单元测试之前,我们要先准备Ant并让我们的构建脚本可以处理单元测试。Ant有一个内置的JUnit目标,但是我们需要把junit.jar放入Antlib目录中。我使用了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要依赖于HttpServletRequestHttpServletResponse以及我们的应用程序上下文。由于控制器并没有使用请求和响应,我们直接传送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”和“viewResolverbean条目,因为这个测试不需要他们。

springapp/src/tests/WEB-INF/springapp-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
 
<!--
  - Application context definition for "springapp" DispatcherServlet.
  -->
 
 
<beans>
    <bean id="springappController" class="web.SpringappController">
        <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>
 
</beans>        

当你运行这个测试的时候,你应该看到载入应用程序上下文时有很多日志信息。

21ProductManager添加单元测试和新的功能

接下来我为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方法中建立了产品信息并且把他们添加到了产品管理对象中。我还给getProductsincreasePrice添加了测试。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类,所以这些类测试起来很方便。

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

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app PUBLIC '-//Sun Microsystems, 
Inc.//DTD Web Application 2.3//EN' 'http://java.sun.com/dtd/web-app_2_3.dtd'>
 
<web-app>
 
    <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>
 
  <taglib>
    <taglib-uri>/spring</taglib-uri>
    <taglib-location>/WEB-INF/spring.tld</taglib-location>
  </taglib>
 
</web-app>

我们还需要在jsp文件的page指令中申明这个taglib。我们用普通的方法通过<form>标签声明一个表单,以及一个<input>文本域和一个提交按钮。

springapp/war/WEB-INF/jsp/priceincrease.jsp

<%@ include file="/WEB-INF/jsp/include.jsp" %>
<%@ taglib prefix="spring" uri="/spring" %>
 
<html>
<head><title><fmt:message key="title"/></title></head>
<body>
<h1><fmt:message key="priceincrease.heading"/></h1>
 
<form method="post">
  <table width="95%" bgcolor="f8f8ff" border="0" cellspacing="0" cellpadding="5">
    <tr>
      <td alignment="right" width="20%">Increase (%):</td>
      <spring:bind path="priceIncrease.percentage">
        <td width="20%">
          <input type="text" name="percentage" value="<c:out value="${status.value}"/>">
        </td>
        <td width="60%">
          <font color="red"><c:out value="${status.errorMessage}"/></font>
        </td>
      </spring:bind>
    </tr>
  </table>
  <br>
  <spring:hasBindErrors name="priceIncrease">
    <b>Please fix all errors!</b>
  </spring:hasBindErrors>
  <br><br>
  <input type="submit" alignment="center" value="Execute">
</form>
<a href="<c:url value="hello.htm"/>">Home</a>
</body>
</html>

<spring:bind>标记是用于将一个<input>表单元素绑定到一个命令对象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

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
 "http://www.springframework.org/dtd/spring-beans.dtd">
 
<!--
  - Application context definition for "springapp" DispatcherServlet.
  -->
 
<beans>
 
    <!--  Controller for the initial "Hello" page -->
    <bean id="springappController" class="web.SpringappController">
        <property name="productManager">
            <ref bean="prodMan"/>
        </property>
    </bean>
 
    <!--  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>
 
    <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>
 
    <bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/hello.htm">springappController</prop>
                <prop key="/priceincrease.htm">priceIncreaseForm</prop>
            </props>
        </property>
    </bean>
 
    <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>
</beans>

下面,让我们看一下这个表单的控制器。onSubmit方法获取了控制并且在它调用ProductManager对象的increasePrice方法之前进行了一些日志记录。然后它使用successViewurl创建了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" %>
  
<html>
<head><title><fmt:message key="title"/></title></head>
<body>
<h1><fmt:message key="heading"/></h1>
<p><fmt:message key="greeting"/> <c:out value="${model.now}"/>
</p>
<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>
  <br>
<a href="<c:url value="priceincrease.htm"/>">Increase Prices</a>
<br>
</body>
</html>

编译并部署,在重新载入应用之后,我们就可以再测试它了。下面是表单出错时所显示的样子:

Back

第4部分 - 实现数据库持久

在第一部分(第1 – 12步)中,我们配置了开发环境并建立了一个基本的应用程序,并以此开始。第二部分(第13 – 19步)在某些方面改进了应用程序。第三部分(第20 – 22步)为应用程序添加了一些单元测试并且我们还添加一个表单用来进行价格增加。在第四部分中,我们要处理数据库持久的问题了。我们在前几部分中看到了我们如何使用在配置文件中bean定义来载入一些业务对象。很显然在实际生活中是这个实行不同的——一旦我们重启了服务器,我们就又回到了原来的价格。我们需要添加可以实际在数据中持久这些更改的代码。

23步 – 添加Ant任务来创建和载入测试数据

在我们开始开发持久部分的代码之前,我们应该先创建我们开发和测试所需的数据库表。我们当然还需要一个数据库。我打算使用HSQL,这是一个很优秀的用Java写的开源数据库。这个数据库和Spring一起分发的,所以我们只要把jar文件复制到web应用的库目录中就可以了。复制spring-framework-1.2/lib/hsqldb/hsqldb.jarspringapp/war/WEB-INF/lib/中。我想让HSQL以独立的模式运行。这样我们就不用担心每次启动一个单独数据库服务器了。用来连接到HSQLURL可以控制数据库运行的模式。为了能够运行一些针对数据库的Ant任务,我们还要在build.properties文件中添加一些数据库的属性。

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
 
 
 
db.driver=org.hsqldb.jdbcDriver
db.url=jdbc:hsqldb:db/test
db.user=sa
db.pw=

下面我要为构建脚本添加我需要的目标。有创建和删除表格以及载入和删除数据库目标。Next I add the targets I need to the build script. There are targets to create and delete tables and to load and delete test data.

    <target name="createTables">
        <echo message="CREATE TABLES USING: ${db.driver} ${db.url}"/>
        <sql driver="${db.driver}"
             url="${db.url}"
             userid="${db.user}"
             password="${db.pw}"
             onerror="continue">  
            <classpath refid="master-classpath"/>
 
        CREATE TABLE products (
          id INTEGER NOT NULL PRIMARY KEY,
          description varchar(255),
          price decimal(15,2)
        );
        CREATE INDEX products_description ON products(description);
 
        </sql> 
    </target>
 
    <target name="dropTables">
        <echo message="DROP TABLES USING: ${db.driver} ${db.url}"/>
        <sql driver="${db.driver}"
             url="${db.url}"
             userid="${db.user}"
             password="${db.pw}"
             onerror="continue">  
            <classpath refid="master-classpath"/>
 
        DROP TABLE products;
 
        </sql> 
    </target>
 
    <target name="loadData">
        <echo message="LOAD DATA USING: ${db.driver} ${db.url}"/>
        <sql driver="${db.driver}"
             url="${db.url}"
             userid="${db.user}"
             password="${db.pw}"
             onerror="continue">  
            <classpath refid="master-classpath"/>
 
        INSERT INTO products (id, description, price) values(1, 'Lamp', 5.78);
        INSERT INTO products (id, description, price) values(2, 'Table', 75.29);
        INSERT INTO products (id, description, price) values(3, 'Chair', 22.81);
 
  
        COMMIT;
 
  
        SHUTDOWN
 
  
  
 
  
        </sql> 
    </target>
 
    <target name="printData">
        <echo message="PRINT DATA USING: ${db.driver} ${db.url}"/>
        <sql driver="${db.driver}"
             url="${db.url}"
             userid="${db.user}"
             password="${db.pw}"
             onerror="continue"
             print="true">  
            <classpath refid="master-classpath"/>
 
        SELECT * FROM products;
 
        </sql> 
    </target>
 
    <target name="clearData">
        <echo message="CLEAR DATA USING: ${db.driver} ${db.url}"/>
        <sql driver="${db.driver}"
             url="${db.url}"
             userid="${db.user}"
             password="${db.pw}"
             onerror="continue">  
            <classpath refid="master-classpath"/>
 
        DELETE FROM products;
 
        </sql> 
    </target>

现在我将执行其中一些任务来建立测试数据库。这会在springapp目录下创建一个db目录。运行“ant createTables loadData pringData”——我把我的输出列在下面了。

[trisberg@localhost springapp]$ ant createTables loadData printData
Buildfile: build.xml
 
createTables:
     [echo] CREATE TABLES USING: org.hsqldb.jdbcDriver jdbc:hsqldb:db/test
      [sql] Executing commands
      [sql] 2 of 2 SQL statements executed successfully
 
loadData:
     [echo] LOAD DATA USING: org.hsqldb.jdbcDriver jdbc:hsqldb:db/test
      [sql] Executing commands
      [sql] 3 of 3 SQL statements executed successfully
 
printData:
     [echo] PRINT DATA USING: org.hsqldb.jdbcDriver jdbc:hsqldb:db/test
      [sql] Executing commands
      [sql] ID,DESCRIPTION,PRICE
      [sql] 1,Lamp,5.78
      [sql] 2,Table,75.29
      [sql] 3,Chair,22.81
 
      [sql] 1 of 1 SQL statements executed successfully
 
BUILD SUCCESSFUL
Total time: 2 seconds

24步 – 为JDBC创建一个数据访问对象(DAO)的实现

我先新建一个“springapp/src/db”目录来存放我用来进行数据库访问的类。在这个目录中我创建了一个叫做“ProductManagerDao.java”的接口。这是定义了DAO实现类需要提供的功能的接口——我们可以有多种实现。

springapp/src/db/ProductManagerDao.java

package db;
 
import bus.Product;
import java.util.List;
 
public interface ProductManagerDao {
 
    public List getProductList();
 
    public void increasePrice(Product prod, int pct);
 
}

我们按照它建立一个叫做“ProductManagerDaoJdbc.java”的类,它是一个上面接口的JDBC的实现。Spring提供了一个JDBC抽象框架,我们可以利用它。直接使用JDBC和使用SpringJDBC框架之间最大的区别是,你不必关心打开和关闭连接或者是任何语句。Spring的框架会帮你进行处理。另一个好处是你不用捕获任何异常,除非你需要。Spring会把所有SQLException包装在从DataAccessException中继承的不检测的异常层次中。如果需要,你也可以捕获这个异常,但由于大多数数据库异常是无法恢复的,所以你可能会就让异常传到更高的层次中去。

springapp/src/db/ProductManagerDaoJdbc.java

package db;
 
import bus.Product;
import java.util.List;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import javax.sql.DataSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.jdbc.object.MappingSqlQuery;
import org.springframework.jdbc.object.SqlUpdate;
import org.springframework.jdbc.core.SqlParameter;
 
public class ProductManagerDaoJdbc implements ProductManagerDao {
 
    /** Logger for this class and subclasses */
    protected final Log logger = LogFactory.getLog(getClass());
 
    private DataSource ds;
 
    public List getProductList() {
        logger.info("Getting products!");
        ProductQuery pq = new ProductQuery(ds);
        return pq.execute();
    }
 
    public void increasePrice(Product prod, int pct) {
        logger.info("Increasing price by " + pct + "%");
        SqlUpdate su = 
            new SqlUpdate(ds, "update products set price = price * (100 + ?) / 100 where id = ?");
        su.declareParameter(new SqlParameter("increase", Types.INTEGER));
        su.declareParameter(new SqlParameter("ID", Types.INTEGER));
        su.compile();
        Object[] oa = new Object[2];
        oa[0] = new Integer(pct);
        oa[1] = new Integer(prod.getId());
        int count = su.update(oa);
        logger.info("Rows affected: " + count);
    }
 
    public void setDataSource(DataSource ds) {
        this.ds = ds;
    }
 
    class ProductQuery extends MappingSqlQuery {
 
        ProductQuery(DataSource ds) {
            super(ds, "SELECT id, description, price from products");
            compile();
        }
 
        protected Object mapRow(ResultSet rs, int rowNum) throws SQLException {
            Product prod = new Product();
            prod.setId(rs.getInt("id"));
            prod.setDescription(rs.getString("description"));
            prod.setPrice(new Double(rs.getDouble("price")));
            return prod;
        }
 
    }
 
}

让我们回顾一下这个类中的两个DAO方法。首先,getProductList()创建一个获取所有产品信息的查询对象。执行了查询后,结果会作为Products的一个列表返回。在类的末尾我们可以看到这个查询类的定义。我把它作为一个内部类实现了。这个查询类扩展了MappingSqlQuery,这是Spring JDBC框架的一个核心类。这个类的用法是你必须在建立类的时候指定一个SQL查询,然后你也要负责实现mapRow方法来把每一行的数据映射到代表查询中所取得的实体的类中。好了,这就是你必须要提供的东西。剩下的东西就交给框架去管吧。在ProductQuery类的构造器中,我传入了数据源。这个数据源将通过一个与我们在第二部分中引入业务对象的方法类似的形式给出,所以我们无须担心如何在DAO类中获取一个数据源。在构造器中我同样定义了我们要用来获取产品的SQL查询。它会创建一个新的Product对象并根据从数据集的当前行获取的数据组装它。你不许要对数据集调用getNext方法,这些都由框架来处理。这也是控制反转(IoC)方法的另一个例子。

第二个方法increasePrice要利用一个SqlUpdate类。同时把数据源和一个带有占位符的SQL更新语句作为参数传给这个类。SQL语句的语法和JDBC中的预处理语句的语法是一样的。事实上,SqlUpdate在幕后就会创建这个预处理语句。对于SQL语句中的参数,我们要给他们名称并且申明他们的类型这样框架才能在执行预处理语句之前正确设置这些参数。在声明了所有的参数之后,我们按照一个JDO的形式对语句进行“编译”。这会通知我们已经完成参数声明,然后框架会检查并确保我们对SQL语句中的每个占位符都有一个匹配的声明。下面我们声明一个Object数组用来存放参数值,并把它们传入预处理语句中。这个数组将被传入SqlUpdateupdate方法中。update方法则会返回受影响的行数。

我需要把每一个产品的主键的值存储在Product类中。当我持久化这个对象的更改到数据库的时候,就会用到这个键。为了存放这个键,我在Product.java中添加了一个叫做“id”的私有字段,并且加入了设置器和获取器。

springapp/src/bus/Product.java

package bus;
 
import java.io.Serializable;
 
public class Product implements Serializable {
 
    private int id;
    private String description;
    private Double price;
 
    public void setId(int i) {
        id = i;
    }
 
    public int getId() {
        return id;
    }
 
    public void setDescription(String s) {
        description = s;
    }
 
    public String getDescription() {
        return description;
    }
 
    public void setPrice(Double d) {
        price = d;
    }
 
    public Double getPrice() {
        return price;
    }
 
}

现在可以测试整个DAO装置了。事实上,我们也许应该首先写这些测试,但是由于这是一个教程风格的文档,所以我想在测试之前先介绍实际的代码会更有意义。我决定使用一个现场的数据库进行测试,所以我要再build.xml中给junit任务添加对clearDataloadData的依赖。这将确保我们的测试都会有一个一致的起点。

    <target name="junit" depends="build,clearData,loadData" 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">
        ***********************************************************
        ***********************************************************
        ****  One or more tests failed!  Check the output ...  ****
        ***********************************************************
        ***********************************************************
        </fail>
    </target>

下面,我给单元测试集合添加了一个TestProductManagerDaoJdbc.java。在启动部分我创建了一个用于测试的数据源。

springapp/src/test/TestProductManagerDaoJdbc.java

package tests;
 
import java.util.List;
import java.util.ArrayList;
import junit.framework.TestCase;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import db.ProductManagerDaoJdbc;
import bus.Product;
 
public class TestProductManagerDaoJdbc extends TestCase {
 
    private ProductManagerDaoJdbc pmdao;
 
    public void setUp() {
        pmdao = new ProductManagerDaoJdbc();
        DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setDriverClassName("org.hsqldb.jdbcDriver");
        ds.setUrl("jdbc:hsqldb:db/test");
        ds.setUsername("sa");
        ds.setPassword("");
        pmdao.setDataSource(ds);
    }
 
    public void testGetProductList() {
        List l = pmdao.getProductList();
        Product p1 = (Product) l.get(0);
        assertEquals("Lamp", p1.getDescription());
        Product p2 = (Product) l.get(1);
        assertEquals("Table", p2.getDescription());
    }
 
    public void testIncreasePrice() {
        List l1 = pmdao.getProductList();
        Product p1 = (Product) l1.get(0);
        assertEquals(new Double("5.78"), p1.getPrice());
        pmdao.increasePrice(p1, 10);
        List l2 = pmdao.getProductList();
        Product p2 = (Product) l2.get(0);
        assertEquals(new Double("6.36"), p2.getPrice());
    }
 
}

一旦我们通过了单元测试,我们就可以继续并修改Web应用来使用这些信的数据库持久能力。

25步 – 修改Web应用来使用数据库持久

如果我们合理地构建了我们的应用程序,我们应该只需要更改业务类就可以立用数据库持久。视图和控制器类不应该被修改,因为他们应该部不知道任何业务类的实现细节。所以让我们把持久添加到ProductManager类中就可以了。我添加了一个ProductManagerDao接口的引用以及这个引用的设置器方法。我们实际上使用哪个实现对于ProductManager类是不相关的,同时我们将通过配置选项对他进行设置。

springapp/src/bus/ProductManager.java

package bus;
 
import java.io.Serializable;
import java.util.ListIterator;
import java.util.List;
import db.ProductManagerDao;
 
public class ProductManager implements Serializable {
 
    private ProductManagerDao pmd;
    private List products;
 
    public void setProductManagerDao(ProductManagerDao pmd) {
        this.pmd = pmd;
    }
 
/*  
    public void setProducts(List p) {
        products = p;
    }
*/
 
    public List getProducts() {
        products = pmd.getProductList();
        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));
*/
            pmd.increasePrice(p, pct);
        }
        
    }
 
}

我们不再依赖我们在内存中种产品列表。每当getProducts被调用的时候,我们都会去查询一下数据库。increasePrice方法现在代理调用DAO来增加价格,而新的价格会在下一次我们调用getProducts方法的时候反映出来。

下面我们要为Web应用修改配置文件——springapp-servlet.xml

springapp/war/WEB-INF/springapp-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>
 
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
 "http://www.springframework.org/dtd/spring-beans.dtd">
  
  <!--
  - Application context definition for "springapp" DispatcherServlet.
  -->
 
<beans>
 
    <!--  Controller for the initial "Hello" page -->
    <bean id="springappController" class="web.SpringappController">
        <property name="productManager">
            <ref bean="prodMan"/>
        </property>
    </bean>
 
    <!--  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>
 
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
      <property name="driverClassName"><value>org.hsqldb.jdbcDriver</value></property>
      <property name="url">
        <value>jdbc:hsqldb:/Users/trisberg/projects/springapp/db/test</value>
      </property>
      <property name="username"><value>sa</value></property>
      <property name="password"><value></value></property>
    </bean>
 
    <bean id="prodManDao" class="db.ProductManagerDaoJdbc">
        <property name="dataSource">
            <ref bean="dataSource"/>
        </property>
    </bean>
 
    <bean id="prodMan" class="bus.ProductManager">
        <property name="productManagerDao">
            <ref bean="prodManDao"/>
        </property>
<!--
        <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>
 
    <bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"> 
        <property name="mappings">
            <props>
                <prop key="/hello.htm">springappController</prop>
                <prop key="/priceincrease.htm">priceIncreaseForm</prop>
            </props>
        </property>
    </bean>
 
    <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>
</beans>

我删除了原来传给ProductManager的产品的集合。我把它们替换成了一个DataSource和一个ProductManagerDaoJdbc实现。指定给datasourceURL包含了数据库位置的完整路径(/Users/trisberg/projects/springapp/db/test)。当然你要把它调整为符合你情况的设置。

现在我们可以构建并部署修改过的应用了,运行“ant undeploy deploy”来清除所有的旧类并把它们替换成新的。然后启动Tomcat并载入应用。你可以看到的区别是小数被限制为2位,这是因为这个列是这样在数据库中被声明的。同时,你应该看到了价格的增加都会保持有效即时在你的应用程序周期结束之后。

26步 – 修复损坏的测试

我们完全更改了ProductManager的实现——我们把所有功能推倒ProductManagerDao实现中,这样原来的测试都失败了。要修复这个问题,我将创建一个ProductManagerDao的模仿实现。这个实现将基本模拟原来我们在ProductManager中的功能。

springapp/src/tests/MockProductManagerDaoImpl.java

package tests;
 
import bus.Product;
import java.util.List;
import db.ProductManagerDao;
import bus.Product;
 
public class MockProductManagerDaoImpl implements ProductManagerDao {
 
    private List products;
 
    public void setProducts(List p) {
        products = p;
    }
 
    public List getProductList() {
        return products;
    }
 
    public void increasePrice(Product prod, int pct) {
        double newPrice = prod.getPrice().doubleValue() * (100 + pct)/100;
        prod.setPrice(new Double(newPrice));
    }
 
}

现在我们要配置“TestSpringappController”和“TestProductManager”测试来使用这个模仿实现。对于“TestSpringappController”我将修改我复制到tests/WEB-INF目录下的“springapp-servlet.xml”文件(我知道对这个文件作一个副本是个很好的主意)。主意不要修改war/WEB-INF中的那个文件——我们需要它保持原样这样我们可以在Tomcat中运行应用程序。我们把产品列表从ProductManager移到ProductManagerDao并且给ProductManager一个指向这个DAO的引用。

springapp/src/tests/WEB-INF/springapp-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
 
<!--
  - Application context definition for "springapp" DispatcherServlet.
  -->
 
 
<beans>
    <bean id="springappController" class="web.SpringappController">
        <property name="productManager">
            <ref bean="prodMan"/>
        </property>
    </bean>
 
    <bean id="prodManDao" class="tests.MockProductManagerDaoImpl">
        <property name="products">
            <list>
                <ref bean="product1"/>
                <ref bean="product2"/>
                <ref bean="product3"/>
            </list>
        </property>
    </bean>
 
    <bean id="prodMan" class="bus.ProductManager">
        <property name="productManagerDao">
            <ref bean="prodManDao"/>
        </property>
<!--
        <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>
 
</beans>        

对于TestProductManager测试案例我们将对setUp方法做一个类似的修改。

springapp/src/tests/TestProductManager .java

package tests;
 
 
import java.util.List;
import java.util.ArrayList;
import junit.framework.TestCase;
 
import db.ProductManagerDao;
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);
*/
        MockProductManagerDaoImpl pmdao = new MockProductManagerDaoImpl();
        pmdao.setProducts(al);
        pm.setProductManagerDao(pmdao);
        pm.getProducts();
    }
 
    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());
    }
 
}

把所有东西辨析并运行测试。他们现在应该可以成功运行了。

上面还有一些东西我想改进一下。一开始,我们并没有使用一个连接池,而这对于任何正式的Web应用部署都是一个必不可少的东西。我们使用了HSQL的独立模式,这限制我们每次只能有一个连接。这当然不行。另一个我们没有处理的问题是并发性。两个用户可以同时增加价格,然后他们可能会对他们两者的行动的效果惊讶。现在我把这些改进留给你去完成——我也可能在将来把这些内容作为本文档的一个附加部分发表。

返回顶部

 

版权所有:UML软件工程组织