Spring MVC学习指南(第2版)

978-7-115-44759-3
作者: 【美】Paul Deck
译者: 林仪明
编辑: 陈冀康
分类: Spring

图书目录:

详情

本书是一本Spring MVC的教程,帮助读者学习和掌握这一框架,快速开发Web应用程序。本书还介绍了Spring MVC的所有和企业Web开发先关的内容。本书的作者是一位资深的程序员,本书针对每一位想要使用Spring MVC开发基于Java的Web应用的读者,进行了非常细致的讲解和剖析。

图书摘要

版权信息

书名:Spring MVC学习指南(第2版)

ISBN:978-7-115-44759-3

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著    [美]Paul Deck

  译    林仪明

  责任编辑 陈冀康

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


Simplified Chinese translation copyright ©2017 by Posts and Telecommunications Press

ALL RIGHTS RESERVED

Spring MVC A Tutorial,Second Edition by Paul Deck

Copyright © 2017 by Brainy Software Inc.

本书中文简体版由作者Paul Deck授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式或任何手段复制和传播。

版权所有,侵权必究。


Spring MVC是Spring框架中用于Web应用快速开发的一个模块,其中的MVC是Model-View-Controller的缩写。作为当今业界最主流的Web开发框架,Spring MVC已经成为当前最热门的开发技能,同时也广泛用于桌面开发领域。

本书重在讲述如何通过Spring MVC来开发基于Java的Web应用。全书共包括13章和5个附录,分别从Spring框架、模型2和MVC模式、Spring MVC介绍、控制器、数据绑定和表单标签库、转换器和格式化、验证器、表达式语言、JSTL、国际化、上传文件、下载文件以及应用测试等多个角度介绍了Spring MVC。附录部分分别介绍了Tomcat、Spring Tool Suite和Maven等工具的安装和配置,Servlet、JavaServer Pages和部署描述符的相关参考资料。除此之外,本书还配有丰富的示例以供读者练习和参考。

本书是一本Spring MVC的教程,内容细致、讲解清晰,非常适合Web开发者和想要使用Spring MVC开发基于Java的Web应用的读者阅读。


Paul Deck是一位资深的Java和Spring专家,开发过大型的企业级应用,并且目前是一位独立的软件工程师。他是 《How Tomcat Works》一书的合著者。


林仪明,男,现为IBM高级工程师。2004年毕业于厦门大学软件学院,主要研究软件架构、应用中间件。目前在福州生活和工作,先后从事软件技术开发,软件架构设计以及团队管理等工作,有多年的开发设计和管理经验,目前提供IBM中间件产品支持工作。


欢迎阅读本书。

Spring MVC是Spring框架中用于Web应用快速开发的一个模块。Spring MVC的MVC是Model-View-Controller的缩写。它是一个广泛应用于图形化用户交互开发中的设计模式,不仅常见于Web开发,也广泛应用于如Swing和JavaFX等桌面开发。

作为当今业界最主流的Web开发框架,Spring MVC(有时也称Spring Web MVC)的开发技能相当热门。本书可供想要学习如何通过Spring MVC开发基于Java的Web应用的开发人员阅读。

Spring MVC基于Spring框架、Servlet和JSP(JavaServer Page),在掌握这3种技术的基础上学习Spring MVC将非常容易。本书第1章针对Spring新手提供一个快速教程,附录C和附录D将帮助你快速学习Servlet和JSP。如果希望深入学习Servlet和JSP,推荐阅读由Budi Kurniawan所著的《Servlet and JSP:A Tutorialsecond Edition》。

接下来,我们将介绍HTTP、基于Servlet和JSP的Web编程,以及本书的章节结构。

HTTP使得Web服务器与浏览器之间可以通过互联网或内网进行数据交互。作为一个制定标准的国际社区,万维网联盟(W3C)负责和维护HTTP。HTTP第一版是HTTP 0.9,随后更新为HTTP 1.0,之后的版本是HTTP 1.1。HTTP 1.1版本的 RFC编号是2616。编写本书时,HTTP 1.1依然是当前最流行的HTTP版本。当前最新版本是发布于2015年5月的HTTP/2,附表1.1列出了HTTP各个版本及其发布时间,HTTP的第二个主要版本通常表示为HTTP/2,而不是HTTP2。

附表1.1 HTTP版本与发布日期

版本

发布时间

HTTP 0.9

1991年

HTTP 1.0

1996年

HTTP 1.1

1997年发布,1999年更新

HTTP/2

2015年5月

Web服务器每天24小时不间断运行,并等待HTTP客户端(通常是Web浏览器)来连接并请求资源。通常,客户端发起一个连接,服务端不会主动连接客户端。

互联网用户需要通过点击链接或者输入一个URL地址来访问一个资源,以下为两个示例:

http://google.com/index.html
http://facebook.com/index.html

URL的第一个部分是HTTP,代表所采用的协议。除HTTP外,URL还可以采用其他类型的协议,以下为两个示例:

mailto:joe@example.org
ftp://marketing@ftp.example.org

通常,HTTP的URL格式如下:

protocol://[host.]domain[:port][/context][/resource][?query string | path variable]

或者

protocol://IP Address[:port][/context][/resource][?query string | path variable]

中括号中的内容是可选项。因此,一个最简单的URL是http://yahoo.ca或者是http://192.168.1.9

需要说明的是,除了输入http://google.com外,还可以用http://173.194.46.35来访问谷歌。可以用ping命令来获取域名对应的IP地址。

ping google.com

由于IP地址不容易记忆,所以实践中更倾向于使用域名。一台计算机可以托管不止一个域名,因此,不同的域名可能指向同一个IP。另外,example.com或者example.org无法被注册,因为它们被保留作为各类文档手册举例使用。

URL中的host部分用来表示在互联网或内网中一个唯一的地址。例如,http://yahoo.com(没有host)访问的地址完全不同于http://mail.yahoo.com(有host)。多年以来,作为最受欢迎的主机名,www是默认的主机名。通常,http://www.domainName会被映射到http://domainName

HTTP的默认端口是80端口。因此,对于采用80端口的Web服务器,无需输入端口号。有时,Web服务器并未运行在80端口上,此时必须输入相应的端口号。例如,Tomcat服务器的默认端口号是8080,为了能正确访问服务器,必须提供输入端口号。

http://localhost:8080/index.html

localhost作为一个保留关键字,用于指向本机。

URL中的context部分用来代表应用名称,该部分也是可选的。一台Web服务器可以运行多个上下文(应用),其中一个可以配置为默认上下文。若访问默认上下文中的资源,可以跳过context部分。

最后,一个context可以有一个或多个默认资源(通常为index.html、index.htm或者default.htm)。一个不带资源名称的URL通常指向默认资源。当存在多个默认资源时,其中最高优先级的资源将被返回给客户端。

资源名后可以有一个或多个查询语句或者路径参数。查询语句是一个key/value组,多个查询语句间用“&”符号分隔。路径参数类似于查询语句,但只有value部分,多个value部分用“/”符号分隔。

接下来,我们介绍HTTP请求和响应。

一个HTTP请求包含3部分内容。

1.方法—URI—协议/版本。

2.请求头信息。

3.请求正文。

下面为一个HTTP请求示例。

POST /examples/default.jsp HTTP/1.1
Accept: text/plain; text/html
Accept-Language:en-gb
Connection:Keep-Alive
Host:localhost
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6
Content-Length:30
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate

lastName=Blanks&firstName=Mike

请求的第一行POST /examples/default.jsp HTTP/1.1是方法—URI—协议/版本。

请求方法为POST,URI为/examples/default.jsp,而协议/版本为HTTP/1.1。

HTTP 1.1规范定义了7种类型的方法,包括GET、POST、HEAD、OPTIONS、PUT、DELETE以及TRACE,其中GET和POST广泛应用于互联网。

URI定义了一个互联网资源,通常解析为服务器根目录的相对路径。因此,通常用“/”符号打头。另外,URL是URI的一个具体类型(详见http://www.ietf.org/rfc/rfc2396.txt)。

HTTP请求包含的请求头信息,包括关于客户端环境以及实体内容等非常有用的信息。例如,浏览器设置的语言、实体内容长度等。每个header都用回车/换行(即CRLF)分隔。

HTTP请求头信息和请求正文用一行空行分隔,HTTP服务器据此判断请求正文的起始位置。因此,在一些关于互联网的书籍中,CRLF被作为HTTP请求的第4种组件。

示例中,请求正文是lastName=Blanks&firstName=Mike。

在正常的HTTP请求中,请求正文的内容不止如此。

同HTTP请求一样,HTTP响应也包含3部分。

1.协议—状态码—描述。

2.响应头信息。

3.响应正文。

下面为一个HTTP响应示例。

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Date: Thu, 29 Sep 2013 13:13:33 GMT
Content-Type: text/html
Last-Modified: Web, 28 Sep 2013 13:13:12 GMT
Content-Length: 112

<html>
<head>
<title>HTTP Response Example</title>
</head>
<body>
Welcome to Brainy Software
</body>
</html>

类似于HTTP请求报文,HTTP响应报文的第一行说明了HTTP的版本是1.1,并且请求结果是成功的(状态代码200为响应成功)。

同HTTP请求报文头信息一样,HTTP响应报文头信息也包含了大量有用的信息。HTTP响应报文的响应正文是HTML文档。HTTP响应报文的头信息和响应正文也是用CRLF分隔的。

状态代码200表示Web服务器能正确响应所请求的资源。若一个请求的资源不能被找到或者理解,则Web服务器将返回不同的状态代码。例如,访问未授权的资源将返回401,而使用被禁用的请求方法将返回405。完整的HTTP响应状态代码列表详见网址http://www.w3. org/Protocols/rfc2616/rfc2616-sec10.html

Java Servlet技术是Java体系中用于开发Web应用的底层技术。1996年,Servlet和JSP由SUN系统公司发布,以替代CGI技术,作为产生Web动态内容的标准。CGI技术为每一个请求创建相应的进程,但是,创建进程会耗费大量的CPU周期,最终导致很难编写可伸缩的CGI程序。相对于CGI程序,一个Servlet则快多了,这是因为当一个Servlet为响应第一次请求而创建后,会驻留在内存中,以便响应后续请求。

从Servlet技术出现的那天起,人们开发了大量的Web框架来帮助程序员快速编写Web应用程序。这些开发框架让开发人员能更关注业务逻辑,减少编写“相似”的代码片段。尽管如此,开发人员依然需要去理解Servlet技术的基础概念。随后发布的JavaServer Pages(JSP)技术,是用来帮助简化Servlet开发。尽管实践中会应用一些诸如Spring MVC、Struct 2或者JSF等强大的开发框架,但如果没有深入理解Servlet和JSP技术,则无法有效和高效地开发。Servlet是运行在Servlet容器中的Java程序,而Servlet容器或Servlet引擎相当于一个Web服务器,但是可以产生动态内容,而不仅是静态资源。Servlet当前的版本为3.1,其规范定义可见JSR (Java Specification Request)340(http://jcp.org/en/jsr/detail?id=340),基于Java标准版本6及以上版本。JSP 2.3规范定义在JSR 245(http://jcp.org/en/jsr/detail?id=245)。本书假定读者已经了解Java以及面向对象编程技术。对于Java新手,推荐阅读《Java 7:A Beginner’s Tutorial(fourth Edition)》。

一个Servlet是一个Java程序,一个Servlet应用包含了一个或多个Servlet,一个JSP页面会被翻译并编译成一个Servlet。

一个Servlet应用运行在一个Servlet容器中,它无法独立运行。Servlet容器将来自用户的请求传递给Servlet应用,并将Servlet应用的响应返回给用户。由于大部分Servlet应用都会包含一些JSP界面,故称Java Web应用为“Servlet/JSP”应用会更恰当些。

Web用户通过一个诸如IE、火狐或者Chrome等Web浏览器来访问Servlet应用。Web浏览器又称为Web客户端。图1展示了一个典型的Servlet/JSP应用架构。

图1 Servlet/JSP应用架构

Web服务端和Web客户端基于HTTP通信。因此,Web服务端往往也称为HTTP服务端。

一个Servlet/JSP容器是一个能处理Servlet以及静态资源的Web服务端。过去,由于HTTP服务器更加健壮,人们更愿意将Servlet/JSP容器作为HTTP服务器(如Apache HTTP服务器)的一个模块来运行,在这种场景下,Servlet/JSP容器用来产生动态内容,而HTTP服务器处理静态资源。今天,Servlet/JSP容器已经更加成熟,并且被广泛地独立部署。Apache Tomcat和Jetty作为最流行的Servlet/JSP容器,免费而且开源。下载地址为http://tomcat.apache.orghttp://jetty.codehaus.org

Servlet和JSP仅是Java企业版众多技术之一,其他Java企业版技术包括JMS、EJB、JSF和JPA等。Java企业版7(当前最新版)完整的技术列表见Http://www.oracle.com/technetwork/java/ javaee/tech/index.html

运行一个Java企业版应用需要一个Java企业版容器,常见的容器有GlassFish、JBoss、Oracle Weblogic以及IBM WebSphere。虽然可以将一个Servlet/JSP应用部署到Java企业版容器中,但一个Servlet/JSP容器其实就足够了,而且更加轻量级。Tomcat和Jetty不是Java企业版容器,故它们不能运行EJB或JMS。

Spring网站建议使用Maven和Gradle来下载Spring库及其依赖包。现在的应用通常有很多依赖包,并且这些依赖包也有自己的依赖。通过一个依赖管理工具,可以把我们从解析和下载依赖的工作中解放出来。并且这些工具还可以帮助构建、测试和打包应用。Maven和Gradle是当下流行的依赖管理系统,使用这两个工具是一个明智的选择。然而当下还有很多Java开发者要么不熟悉这两个工具,要么不喜欢使用或者不想使用这两个工具。一个好的教程不能要求他们先学习Maven或Gradle,然后才能构建和测试他们的Spring应用程序。因此,本书所有的例子都有两种发行包:基于Maven的发行包和不依赖Maven/Gradle的发行包。

Maven曾经是Spring支持的唯一的依赖管理系统。Spring开发工具Spring Tool Suites(STS),是一个基于Eclipse的IDE,捆绑了一个最新的Maven版本。这对于不熟悉Maven的开发人员是一个好消息,只要熟悉Eclipse,则无须学习Maven就可以应用其好处。然而,在写作本书的时候,最新版本的STS并不是没有缺点。例如,当使用STS创建Spring MVC应用程序时,生成的应用程序会因为缺少Servlet部署描述符(web.xml文件)而导致Maven插件生成一个错误消息。因此开发人员不得不被迫处理一个Maven问题。当然,可以通过简单地创建一个web.xml文件来解决它。但是,从Servlet 3.0开始,web.xml文件是可选的,并且可以构建一个没有web.xml文件的Spring MVC应用程序。

下面对于使用Maven/Gradle或不使用工作会分别介绍。

如果不想通过Gradle和Maven来构建应用,则应从Spring仓库中下载其类库:

http://repo.spring.io/release/org/springframework/spring/ 

点击以上链接将跳转到最新版本(写作本书时为版本为4.2.5),选择一个zip发行包,发行包命名格式如下:

spring-framework-x.y.z.RELEASE-dist.zip 

其中,x.y.z表示Spring的主要版本和次要版本。

将下载好的zip解压到任意目录。在解压的目录中,包含相应的文档和Java源代码,其中libs文件夹下为基于Spring框架开发应用所需的jar文件。

如果选择使用Maven或Gradle,则无需手动下载Spring。具体内容,请参考本书附录B。

Spring框架是一个开源项目,如果你喜欢冒险或者你想要的尚未发布的最新版本的Spring,你可以使用Git下载源代码。克隆Spring源代码的命令如下:

git clone git://github.com/spring-projects/spring-framework.git

此外还需使用Gradle来从源代码构建Spring。Git和Gradle工具不是本书的范围。


第1章:Spring框架,介绍了最流行的开源框架。

第2章:模型2和MVC模式,讨论了Spring MVC实现的设计模式。

第3章:Spring MVC介绍,编写了第一个Spring MVC应用。

第4章:基于注解的控制器,讨论了MVC模式中最重要的一个对象——控制器。在本章中,我们将学习如何编写基于注解的控制器,该方式由Spring MVC 2.5版本引入。

第5章:数据绑定和表单标签库,讨论Spring MVC最强大的一个特性,并利用它来展示表单数据。

第6章:转换器和格式化,讨论了数据绑定的辅助对象类型。

第7章:验证器,展示如何通过验证器来验证用户输入数据。

第8章:表达式语言,介绍了JSP 2.0中最重要的特性“表达式语言”。该特性的目标是帮助开发人员编写无脚本的JSP页面,让JSP页面更加简洁而且有效。本章将帮助你学会通过EL来访问Java Bean和上下文对象。

第9章:JSTL,介绍了JSP技术中最重要的类库——标准标签库。这是一组帮助处理常见问题的标签,诸如访问map或集合对象、条件判断、XML处理,以及数据库访问和数据处理。

第10章:国际化,将展示如何用Spring MVC来构建多语言网站。

第11章:上传文件,介绍两种不同的方式来处理文件上传。

第12章:下载文件,介绍如何用编程方式向客户端端传输一个资源。

第13章:应用测试,介绍如何使用Junit、Mockito和Spring MVC Test进行单元测试和集成测试。

附录A:Tomcat,介绍如何安装和配置Tomcat。

附录B: Spring Tool Suite和Maven,介绍如何安装并配置Spring Tool Suite,并且开发和运行Spring MVC应用。

附录C:Servlet,介绍了Servlet API并展示几个简单的Servlet应用。本附录重点关注Servlet API的4个包中的两个,即javax.servlet和javax.servlet.http。

附录D:JavaServer Pages,介绍了JSP语法,包括指令、脚本元素和actions。

附录E:部署描述符,介绍如何配置Spring MVC应用以便部署。

本书所有的示例应用压缩包可以通过以下地址下载:

http://books.brainysoftware.com/downloadhttp://www.epubit.com.cn


Spring框架是一个开源的企业应用开发框架,作为一个轻量级的解决方案,它包含20多个不同的模块。本书主要关注Core、Spring Bean、Spring MVC和Spring MVC Test模块。

本章主要介绍Core和Spring Bean这两个模块,以及它们如何提供依赖注入解决方案。为方便初学者,本书会深入讨论依赖注入概念的细节。后续介绍开发MVC应用的章节将会使用到本章介绍的技能。

在过去数年间,依赖注入技术作为代码可测试性的一个解决方案已经被广泛应用。实际上,Spring、谷歌Guice等框架都采用了依赖注入技术。那么,什么是依赖注入技术?

很多人在使用中并不区分依赖注入和控制反转(IoC),尽管Martin Fowler在其文章中已分析了两者的不同:

http://martinfowler.com/articles/injection.html

简单来说,依赖注入的情况如下。

有两个组件A和B,A依赖于B。假定A是一个类,且A有一个方法importantMethod用到了B,如下:

public class A {
  public void importantMethod() {
    B b = ... // get an instance of B
    b.usefulMethod();
    ...
  }
  ...
}

要使用B,类A必须先获得组件B的实例引用。若B是一个具体类,则可通过new关键字直接创建组件B实例。但是,如果B是接口,且有多个实现,则问题就变得复杂了。我们固然可以任意选择接口B的一个实现类,但这也意味着A的可重用性大大降低了,因为无法采用B的其他实现。

依赖注入是这样处理此类情景的:接管对象的创建工作,并将该对象的引用注入需要该对象的组件。以上述情况为例,依赖注入框架会分别创建对象A和对象B,将对象B注入到对象A中。

为了能让框架进行依赖注入,程序员需要编写特定的set方法或者构建方法。例如,为了能将B注入到A中,类A会被修改成如下形式:

public class A {
  private B b;
  public void importantMethod() {
    // no need to worry about creating B anymore
    // B b = ... // get an instance of B
    b.usefulMethod();
    ...
  }
  public void setB(B b) {
    this.b = b;
  }
}

修改后的类A新增了一个set方法,该方法将会被框架调用,以注入B的一个实例。由于对象依赖由依赖注入,类A的importantMethod方法不再需要在调用B的usefulMethod方法前去创建B的一个实例。

当然,也可以采用构造器方式注入,如下所示:

public class A {
  private B b;

  public A(B b) {
    this.b = b;
  }

  public void importantMethod() {
    // no need to worry about creating B anymore
    // B b = ... // get an instance of B
    b.usefulMethod();
    ...
  }
}

本例中,Spring会先创建B的实例,再创建实例A,然后把B注入到实例A中。

注:

 

Spring管理的对象称为beans。

通过提供一个控制反转容器(或者依赖注入容器),Spring为我们提供一种可以“聪明”地管理Java对象依赖关系的方法。其优雅之处在于,程序员无需了解Spring框架的存在,更不需要引入任何Spring类型。

从1.0版本开始,Spring就同时支持setter和构造器方式的依赖注入。从2.5版本开始,通过Autowired注解,Spring支持基于field方式的依赖注入,但缺点是程序必须引入org.springframework.beans.factory.annotation.Autowired,这对Spring产生了依赖,这样,程序无法直接迁移到另一个依赖注入容器间。

使用Spring,程序几乎将所有重要对象的创建工作移交给Spring,并配置如何注入依赖。Spring支持XML或注解两种配置方式。此外,还需要创建一个ApplicationContext对象,代表一个Spring控制反转容器,org.springframework.context.ApplicationContext接口有多个实现,包括ClassPathXmlApplicationContext和FileSystemXmlApplicationContext。这两个实现都需要至少一个包含beans信息的XML文件。ClassPathXmlApplicationContext尝试在类加载路径中加载配置文件,而FileSystemXmlApplicationContext则从文件系统中加载。

下面是从类路径中加载config1.xml和config2.xml的ApplicationContext创建的一个代码示例。

ApplicationContext context = new ClassPathXmlApplicationContext(
    new String[] {"config1.xml", "config2.xml"});

可以通过调用ApplicationContext的getBean方法获得对象。

Product product = context.getBean("product", Product.class);

getBean方法会查询id为product且类型为Product的bean对象。

注:

 

理想情况下,我们只需在测试代码中创建一个ApplicationContext,应用程序本身无需处理。对于Spring MVC应用,可以通过一个Spring Servlet来处理ApplicationContext,而无需直接处理。

从1.0版本开始,Spring就支持基于XML的配置;从2.5版本开始,增加了通过注解的配置支持。下面介绍如何配置XML文件。配置文件的根元素通常为beans:

< ?xml version="1.0" encoding="UTF-8"?>
< beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://www.springframework.org/schema/beans
 http://www.springframework.org/schema/beans/spring-beans.xsd">

  ...
< /beans>

如果需要更强的Spring配置能力,可以在schema location属性中添加相应的schema,也可以指定schema版本:

http://www.springframework.org/schema/beans/spring-beans-4.2.xsd

不过,笔者推荐使用默认schema,以便升级spring库时无需修改配置文件。

配置文件既可以是一份,也可以分解为多份,以支持模块化配置。ApplicationContext的实现类支持读取多份配置文件。另一种选择是,通过一份主配置文件,将该文件导入到其他配置文件。

下面是导入其他配置文件的一个示例:

< ?xml version="1.0" encoding="UTF-8"?>
< beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://www.springframework.org/schema/beans
 http://www.springframework.org/schema/beans/spring-beans.xsd">

  < import resource="config1.xml"/>
  < import resource="module2/config2.xml"/>
  < import resource="/resources/config3.xml"/>
 ...
< /beans>

bean元素的配置后面将会详细介绍。

本节主要介绍Spring如何管理bean和依赖关系。

前面已经介绍,通过调用ApplicationContext的getBean方法可以获取一个bean的实例。下面的配置文件中定义了一个名为product的bean(见清单1.1)。

清单1.1 一个简单的配置文件

< ?xml version="1.0" encoding="UTF-8"?>
< beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://www.springframework.org/schema/beans
 http://www.springframework.org/schema/beans/spring-beans.xsd">

  < bean name="product" class="springintro.bean.Product"/>

< /beans>

该bean的定义告诉Spring,通过默认无参的构造器来初始化Product类。如果不存在该构造器(如果类的编写者重载了构造器,且没有显示声明默认构造器),则Spring将抛出一个异常。此外,该无参数的构造器并不要求是public签名。

注意,应采用id或者name属性标识一个bean。为了让Spring创建一个Product实例,应将bean定义的name值“product”(具体实践中也可以是id值)和Product类型作为参数传递给ApplicationContext的getBean方法。

ApplicationContext context =
    new ClassPathXmlApplicationContext(
    new String[] {"spring-config.xml"});
Product product1 = context.getBean("product", Product.class);
product1.setName("Excellent snake oil");
System.out.println("product1: " + product1.getName());

大部分类可以通过构造器来实例化。然而,Spring还同样支持通过调用一个工厂的方法来初始化类。

下面的bean定义展示了通过工厂方法来实例化java.time.LocalDate。

<bean id="localDate" class="java.time.LocalDate"
  factory-method="now"/>

本例中采用了id属性而非name属性来标识bean,采用了getBean方法来获取LocalDate 实例。

ApplicationContext context =
    new ClassPathXmlApplicationContext(
    new String[] {"spring-config.xml"});
LocalDate localDate = context.getBean("localDate", LocalDate.class);

有时,我们希望一些类在被销毁前能执行一些方法。Spring考虑到了这样的需求。可以在bean定义中配置destroy-method属性,来指定在销毁前要执行的方法。

下面的例子中,我们配置Spring通过java.util.concurrent.Executors的静态方法newCached ThreadPool来创建一个java.uitl.concurrent.ExecutorService实例,并指定了destroy-method属性值为shutdown方法。这样,Spring会在销毁ExecutorService实例前调用其shutdown方法。

<bean id="executorService" class="java.util.concurrent.Executors"
  factory-method="newCachedThreadPool"
  destroy-method="shutdown"/>

Spring支持通过带参数的构造器来初始化类(见清单1.2)。

清单1.2 Product类

package springintro.bean;
import java.io.Serializable;

public class Product implements Serializable {
  private static final long serialVersionUID = 748392348L;
  private String name;
  private String description;
  private float price;

  public Product() {
  }

  public Product(String name, String description, float price) {
    this.name = name;
    this.description = description;
    this.price = price;
  }
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
  public String getDescription() {
    return description;
  }
  public void setDescription(String description) {
    this.description = description;
  }
  public float getPrice() {
    return price;
  }
  public void setPrice(float price) {
    this.price = price;
  }
}

以下的定义展示了如何通过参数名传递参数。

<bean name="featuredProduct" class="springintro.bean.Product">
  <constructor-arg name="name" value="Ultimate Olive Oil"/>
  <constructor-arg name="description"
    value="The purest olive oil on the market"/>
  <constructor-arg name="price" value="9.95"/>
</bean>

这样,在创建Product实例时,Spring会调用如下构造器:

public Product(String name, String description, float price) {
  this.name = name;
  this.description = description;
  this.price = price;
}

除了通过名称传递参数外,Spring还支持通过指数方式来传递参数,具体如下:

<bean name="featuredProduct2" class="springintro.bean.Product">
  <constructor-arg index="0" value="Ultimate Olive Oil"/>
  <constructor-arg index="1"
    value="The purest olive oil on the market"/>
  <constructor-arg index="2" value="9.95"/>
</bean>

需要说明的是,采用这种方式,对应构造器的所有参数必须传递,缺一不可。

下面以Employee类和Address类为例,介绍setter方式依赖注入(见清单1.3和清单1.4)。

清单1.3 Employee类

package springintro.bean;

public class Employee {
  private String firstName;
  private String lastName;
  private Address homeAddress;

  public Employee() {
  }

  public Employee(String firstName, String lastName, Address
    homeAddress) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.homeAddress = homeAddress;
  }

  public String getFirstName() {
    return firstName;
  }

  public void setFirstName(String firstName) {
    this.firstName = firstName;
  }

  public String getLastName() {
    return lastName;
  }

  public void setLastName(String lastName) {
    this.lastName = lastName;
  }

  public Address getHomeAddress() {
    return homeAddress;
  }

  public void setHomeAddress(Address homeAddress) {
    this.homeAddress = homeAddress;
  }

  @Override
  public String toString() {
    return firstName + " " + lastName
        + "\n" + homeAddress;
  }
}

清单1.4 Address类

package springintro.bean;

public class Address {
 private String line1;
  private String line2;
  private String city;
  private String state;
  private String zipCode;
  private String country;

  public Address(String line1, String line2, String city,
      String state, String zipCode, String country) {
    this.line1 = line1;
    this.line2 = line2;
    this.city = city;
    this.state = state;
    this.zipCode = zipCode;
    this.country = country;
  }

  // getters and setters omitted

  @Override
  public String toString() {
    return line1 + "\n"
        + line2 + "\n"
        + city + "\n"
        + state + " " + zipCode + "\n"
        + country;
  }
}

Employee依赖于Address类,可以通过如下配置来保证每个Employee实例都能包含Address实例。

<bean name="simpleAddress" class="springintro.bean.Address">
  <constructor-arg name="line1" value="151 Corner Street"/>
  <constructor-arg name="line2" value=""/>
  <constructor-arg name="city" value="Albany"/>
  <constructor-arg name="state" value="NY"/>
  <constructor-arg name="zipCode" value="99999"/>
  <constructor-arg name="country" value="US"/>
</bean>

<bean name="employee1" class="springintro.bean.Employee">
  <property name="homeAddress" ref="simpleAddress"/>
  <property name="firstName" value="Junior"/>
  <property name="lastName" value="Moore"/>
</bean>

simpleAddress对象是Address类的一个实例,它通过构造器方式实例化。employee1对象则通过配置property元素来调用setter方法以设置值。需要注意的是,homeAddress属性配置的是simpleAddress对象的引用。

被引用对象的配置定义无需早于引用其对象的定义。在本例中,employee1对象可以出现在simpleAddress对象定义之前。

清单1.3所示的Employee类提供了一个可以传递参数的构造器,我们还可以将Address对象通过构造器注入,如下所示:

<bean name="employee2" class="springintro.bean.Employee">
  <constructor-arg name="firstName" value="Senior"/>
  <constructor-arg name="lastName" value="Moore"/>
  <constructor-arg name="homeAddress" ref="simpleAddress"/>
</bean>

<bean name="simpleAddress" class="springintro.bean.Address">
  <constructor-arg name="line1" value="151 Corner Street"/>
  <constructor-arg name="line2" value=""/>
  <constructor-arg name="city" value="Albany"/>
  <constructor-arg name="state" value="NY"/>
  <constructor-arg name="zipCode" value="99999"/>
  <constructor-arg name="country" value="US"/>
</bean>

本章学习了依赖注入的概念以及基于Spring容器的实践,后续将在此基础之上配置Spring应用。


Java Web应用开发中有两种设计模型,为了方便,分别称为模型1和模型2。模型1是以页面中心,适合于小应用开发。而模型2基于MVC模式,是Java Web应用的推荐架构(简单类型的应用除外)。

本章将会讨论模型2,并展示4个不同示例应用。第一个应用是一个基本的模型2应用,采用Servlet作为控制器;第二个应用也是模型2应用,但采用了Filter作为控制器;第三个应用引入了验证控件来校验用户的输入;最后一个应用则采用了一个自研的依赖注入器。在实践中,应替换为Spring。

 

在写作本书时,业界正致力于MVC Web框架的标准化(见JSR 371)。

第一次学习JSP,通常通过链接方式进行JSP页面间的跳转。这种方式非常直接,但在中型和大型应用中,这种方式会带来维护上的问题。修改一个JSP页面的名字,会导致页面中大量的链接需要修正。因此,实践中并不推荐模型1(但仅2~3个页面的应用除外)。

模型2基于模型—视图—控制器(MVC)模式,该模式是Smalltalk-80用户交互的核心概念,那时还没有设计模式的说法,当时称为MVC范式。

一个实现MVC模式的应用包含模型、视图和控制器3个模块。视图负责应用的展示。模型封装了应用的数据和业务逻辑。控制器负责接收用户输入,改变模型以及调整视图的显示。

模型2中,Servlet或者Filter都可以充当控制器。几乎所有现代Web框架都是模型2的实现。Struts 1、Spring MVC和JavaServer Faces使用一个Servlet作为控制器,而Struts 2则使用一个Filter作为控制器。大部分都采用JSP页面作为应用的视图,当然也有其他技术。而模型则采用POJO(Plain Old Java Object)。不同于EJB等特定对象,POJO是一个普通对象。实践中会采用一个JavaBean来持有模型状态,并将业务逻辑放到一个Action类中。

图2.1展示了一个模型2应用的架构图。

图2.1 模型2架构图

每个HTTP请求都发送给控制器,请求中的URI标识出对应的action。action代表了应用可以执行的一个操作。一个提供了action的Java对象称为action对象。一个action类可以支持多个action(在Spring MVC以及Struts 2中),或者一个action(在Struts 1中)。

看似简单的操作可能需要多个action。如向数据库添加一个产品,需要两个action。

(1)显示一个“添加产品”的表单,以便用户能输入产品信息。

(2)将表单信息保存到数据库中。

如前所述,我们需要通过URI方式告诉控制器执行相应的action。例如,通过发送类似如下的URI,来显示“添加产品”表单。

http://domain/appName/input-product

通过类似如下的URI,来保存产品。

http://domain/appName/save-product

控制器会解析URI并调用相应的action,然后将模型对象放到视图可以访问的区域(以便服务端数据可以展示在浏览器上)。最后,控制器利用RequestDispatcher或者HttpServlet Response.sendRedirect()方法跳转到视图(JSP页面或者其他资源)。在JSP页面中,用表达式语言以及定制标签显示数据。

注意

 

调用RequestDispatcher.forward方法或者HttpServletResponse.sendRedirect()方法并不会停止执行剩余的代码。因此,若forward方法不是最后一行代码,则应显式地返回。

if (action.equals(...)) {
  RequestDispatcher rd = request.getRequestDispatcher(dispatchUrl); 
  rd.forward(request, response);
  return;//explicitly return. Or else, the code below will be executed 
}
// do something else

大多数时候,你将使用RequestDispatcher转发到视图,因为它比sendRedirect更快响应。 这是因为重定向导致服务器向浏览器发送状态代码为302的HTTP响应,并包含新URL。 而浏览器在接收到状态代码302时,根据响应头部中找到的URL向服务器发出新的HTTP请求。 换句话说,重定向需要一个往返,这使其慢于转发。

使用重定向超过转发的优势是什么? 通过重定向,你可以将浏览器定向到其他应用程序,这是转发不能支持的。如果重定向用于在同一应用程序中不同的资源,由于使用与原始请求URL不同的URL,若用户在响应后意外地按下浏览器的重新加载/刷新按钮,则与原始请求URL相关联的代码将不会再次执行。例如,你不希望因为用户意外按下她的浏览器的重新加载或刷新按钮,而导致重新执行诸如信用卡付款的代码。

本章最后一个例子是appdesign4应用程序,它显示了一个重定向的例子。

为了便于对模型2有一个直观的了解,本节将展示一个简单模型2应用。实践中,模型2的应用非常复杂。

示例应用名为appdesign1,其功能设定为输入一个产品信息。具体为:用户填写产品表单(图2.2)并提交;示例应用保存产品并展示一个完成页面,显示已保存的产品信息(见图2.3)。

图2.2 产品表单

图2.3 产品详细页

示例应用支持如下两个action。

(1)展示“添加产品”表单。该action将图2.2中的输入表单发送到浏览器上,其对应的URI应包含字符串input-product。

(2)保存产品并返回如图2.3所示的完成页面,对应的URI必须包含字符串save-product。

示例应用由如下组件构成:

(1)一个Product类,作为product的领域对象。

(2)一个ProductForm类,封装了HTML表单的输入项。

(3)一个ControllerServlet类,本示例应用的控制器。

(4)一个SaveProductAction类。

(5)两个JSP页面(ProductForm.jsp和Product Detail.jsp)作为视图。

(6)一个CSS文件,定义了两个JSP页面的显示风格。

示例应用目录结构如图2.4所示。

图2.4 app02a目录结构

下面详细介绍示例应用的每个组件。

Product实例是一个封装了产品信息的JavaBean。Product类(见清单2.1)包含3个属性:productName、description和price。

清单2.1 Product类

package appdesign1.model;
import java.io.Serializable;
import java.math.BigDecimal;

public class Product implements Serializable {
  private static final long serialVersionUID = 748392348L;
  private String name;
  private String description;
  private BigDecimal price;

  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
  public String getDescription() {
    return description;
  }
  public void setDescription(String description) {
    this.description = description;
  }
  public BigDecimal getPrice() {
    return price;
  }
  public void setPrice(BigDecimal price) {
    this.price = price;
  }
}

Product类实现了java.io.Serializable接口,其实例可以安全地将数据保存到HttpSession中。根据Serializable的要求,Product实现了一个serialVersionUID属性。

表单类与HTML表单相映射,是后者在服务端的代表。ProductForm类(见清单 2.2)包含了一个产品的字符串值。ProductForm类看上去同Product类相似,这就引出一个问题:ProductForm类是否有存在的必要。

实际上,表单对象会传递ServletRequest给其他组件,类似Validator(本章后面会介绍)。而ServletRequest是一个Servlet层的对象,不应当暴露给应用的其他层。

另一个原因是,当数据校验失败时,表单对象将用于保存和展示用户在原始表单上的输入。2.5节将会详细介绍应如何处理。

注意:

 

大部分情况下,一个表单类不需要实现Serializable接口,因为表单对象很少保存在HttpSession中。

清单2.2 ProductForm类

package appdesign1.form;
public class ProductForm {
  private String name;
  private String description;
  private String price;

  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
  public String getDescription() {
    return description;
  }
  public void setDescription(String description) {
    this.description = description;
  }
  public String getPrice() {
    return price;
  }
  public void setPrice(String price) {
    this.price = price;
  }
}

ControllerServlet类(见清单2.3)继承自javax.servlet.http.HttpServlet类。其doGet和doPost方法最终调用process方法,该方法是整个Servlet控制器的核心。

可能有人好奇,为何这个Servlet控制器命名为ControllerServlet,实际上,这里遵从了一个约定:所有Servlet的类名称都带有Servlet后缀。

清单2.3 ControllerServlet类

package appdesign1.controller;
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import appdesign1.action.SaveProductAction;
import appdesign1.form.Product Form;
import appdesign1.model.Product;
import java.math.BigDecimal;

@WebServlet(name = "ControllerServlet", urlPatterns = {
    "/input-product", "/save-product"})
public class ControllerServlet extends HttpServlet {

  private static final long serialVersionUID = 1579L;

  @Override
  public void doGet(HttpServletRequest request, 
      HttpServletResponse response) 
      throws IOException, ServletException {
    process(request, response);
  }

  @Override
  public void doPost(HttpServletRequest request, 
      HttpServletResponse response) 
      throws IOException, ServletException {
    process(request, response);
  }

  private void process(HttpServletRequest request, 
      HttpServletResponse response) 
      throws IOException, ServletException {

    String uri = request.getRequestURI();
    /*
     * uri is in this form: /contextName/resourceName,
     * for example: /appdesign1/input-product.
     * However, in the event of a default context, the
     * context name is empty, and uri has this form
     * /resourceName, e.g.: /input-product
     */
    int lastIndex = uri.lastIndexOf("/");
    String action = uri.substring(lastIndex + 1);
    // execute an action
    String dispatchUrl = null;
    if ("save-product".eauals(action)) {
      // no action class, just forward
      dispatchUrl = "/jsp/ProductForm.jsp";
    } else if ("input-product".eauals(action)) {
      // create form
      ProductForm productForm = new ProductForm();
      // populate action properties
      productForm.setName(request.getParameter("name"));
      productForm.setDescription(
          request.getParameter("description"));
      productForm.setPrice(request.getParameter("price"));

      // create model
      Product product = new Product();
      product.setName(productForm.getName());
      product.setDescription(productForm.getDescription());
      try {
        product.setPrice(new Bigdecimal(productForm.getPrice()));
      } catch (NumberFormatException e) {
      }
      // execute action method
      SaveProductAction saveProductAction = 
          new SaveProductAction();
      saveProductAction.save(product);

      // store model in a scope variable for the view
      request.setAttribute("product", product);
      dispatchUrl = "/jsp/ProductDetails.jsp"; 
    }

    if (dispatchUrl != null) {
      RequestDispatcher rd = 
          request.getRequestDispatcher(dispatchUrl);
      rd.forward(request, response);
    }
  }
}

ControllerServlet的process方法处理所有输入请求。首先是获取请求URI和action名称。

String uri = request.getRequestURI();
int lastIndex = uri.lastIndexOf("/");
String action = uri.substring(lastIndex + 1);

在本示例应用中,action值只会是input-product或save-product。

接着,process方法执行如下步骤。

(1)创建并根据请求参数构建一个表单对象。save-product操作涉及3个属性:name、description和price。然后创建一个领域对象,并通过表单对象设置相应属性。

(2)执行针对领域对象的业务逻辑。

(3)转发请求到视图(JSP页面)。

process方法中判断action的if代码块如下:

// execute an action
if ("input-product".eauals(action))) {
  // no action class, just forward
  dispatchUrl = "/jsp/ProductForm.jsp";
} else if ("save-product".eauals(action)) {
  // instantiate action class
  …
}

对于input-product,无需任何操作,而针对save-product,则创建一个ProductForm对象和Product对象,并将前者的属性值复制到后者。这个步骤中,针对空字符串的复制处理将留到稍后的2.5节处理。

再次,process方法实例化SaveProductAction类,并调用其save方法。

    // create form
    ProductForm productForm = new ProductForm();
    // populate action properties
    productForm.setName(request.getParameter("name"));
    productForm.setDescription(
        request.getParameter("description"));
    productForm.setPrice(request.getParameter("price"));

    // create model
    Product product = new Product();
    product.setName(productForm.getName());
    product.setDescription(productForm.getDescription());
    try {
     product.setPrice(new BigDecimal(productForm.getPrice()));
    } catch (NumberFormatException e) {
    }
    // execute action method
    SaveProductAction saveProductAction = 
        new SaveProductAction();
    saveProductAction.save(product);

然后,将Product对象放入HttpServletRequest对象中,以便对应的视图能访问到。

    // store action in a scope variable for the view
    request.setAttribute("product", product);

最后,process方法转到视图,如果action是product_input,则转到ProductForm.jsp页面,否则转到ProductDetails.jsp页面。

// forward to a view
if (dispatchUrl != null) {
  RequestDispatcher rd = 
      request.getRequestDispatcher(dispatchUrl);
  rd.forward(request, response);
}

这个应用中只有一个action类,负责将一个product持久化,例如数据库。这个action类名为SaveProductAction(见清单2.4)。

清单2.4 SaveProductAction类

package appdesign1.action; 

public class SaveProductAction { 
  public void save(Product product) { 
    // insert Product to the database 
  } 
}

在这个示例中,SaveProductAction类的save方法是一个空实现。我们会在本章后续章节中实现它。

示例应用包含两个JSP页面。第一个页面ProductForm.jsp对应于input-product操作,第二个页面ProductDetails.jsp对应于save-product操作。ProductForm.jsp以及ProductDetails.jsp页面代码分别见清单2.5和清单2.6。

清单2.5 ProductForm.jsp

<!DOCTYPE html>
<html>
<head>
<title>Add Product Form</title>
<style type="text/css">@import url(css/main.css);</style>
</head>
<body>
<form method="post" action="save-product ">
  <h1> Add Product
    <span>Please use this form to enter product details</span>
  </h1>
  <label>
    <span>Product Name: </span>
    <input id="name" type="text" name="name" 
      placeholder="The complete product name">
  </label>
  <label>
    <span>Description: </span>
    <input id="description" type="text" name="description"
      placeholder="Product description">
  </label>
  <label>
    <span>Price: </label>
    <input id="price" name="price" type="number" step="any"
      placeholder="Product price in #.## format">
  </label>
  <label>
    <span>&nbsp: </span>
    <input type="submit">
  </label>
</form>
</body>
</html>

注意

 

不要用HTML Tabel来布局表单,用CSS。

注意

 

价格输入域的step属性要求浏览器允许输入小数数字。

清单2.6 ProductDetails.jsp

<!DOCTYPE html>
<html>
<head>
<title>Save Product</title>
<style type="text/css">@import url(css/main.css);</style>
</head>
<body>
<div id="global">
  <h4>The product has been saved.</h4>
  <p>
    <h5>Details:</h5>
    Product Name: ${product.name}<br/>
    Description: ${product.description}<br/>
    Price: $${product.price}
  </p>
</div>
</body>
</html>

ProductForm.jsp页面包含了一个HTML表单。ProductDetails.jsp页面通过表达式语言(EL)访问HttpServletRequest所包含的product对象。

作为模型2的一个应用,本示例应用可以通过如下几种方式避免用户通过浏览器直接访问JSP页面。

假定示例应用运行在本机的8080端口上,则可以通过如下URL访问应用:

http://localhost:8080/appdesign1/input-product

浏览器将显示图2.2的内容。

完成输入后,表单提交到如下服务端URL上:

http://localhost:8080/appdesign1/save-product

注意

 

可以将servlet控制器作为默认主页。这是一个非常重要的特性,使得在浏览器地址栏中仅输入域名(如http://example.com),就可以访问到该servlet控制器,这是无法通过filter方式完成的。

虽然servlet是模型2应用程序中最常见的控制器,但过滤器也可以充当控制器。但请注意,过滤器没有作为欢迎页面的权限。仅输入域名时不会调用过滤器分派器。Struts 2使用过滤器作为控制器,是因为该过滤器也用于提供静态内容。

下面的例子(appdesign2)是一个采用filter分发器的模型2应用,目录结构如图2.5所示。

图2.5 appdesign2目录结构

JSP页面和Product类同appdesign1相同,但没有采用servlet作为控制器,而是使用了一个名为FilterDispatcher的过滤器(见清单2.7)。

清单2.7 DispatcherFilter类

package appdesign2.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.RequestDispatcher; 
import javax.servlet.ServletException; 
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter; 
import javax.servlet.http.HttpServletRequest; 
import appdesign2.action.SaveProductAction; 
import appdesign2.form.ProductForm; 
import appdesign2.model.Product; 
import java.math.BigDecimal; 

@WebFilter(filterName = "DispatcherFilter", 
    urlPatterns = { "/*" })
public class DispatcherFilter implements Filter { 

  @Override
  public void init(FilterConfig filterConfig) 
      throws ServletException { 
  } 

  @Override
  public void destroy() {
  } 

  @Override
  public void doFilter(ServletRequest request, 
      ServletResponse response, FilterChain filterChain) 
      throws IOException, ServletException { 
    HttpServletRequest req = (HttpServletRequest) request; 
    String uri = req.getRequestURI();
    /* 
     * uri is in this form: /contextName/resourceName, for 
     * example /appdesign2/input-product. However, in the 
     * case of a default context, the context name is empty, 
     * and uri has this form /resourceName, e.g.: 
     * /input-product 
     */
    // action processing
    int lastIndex = uri.lastIndexOf("/");
    String action = uri.substring(lastIndex + 1); 
    String dispatchUrl = null;
    if ("input-product".equals(action)) { 
      // do nothing 
      dispatchUrl = "/jsp/ProductForm.jsp";
    }else if("save-product".equals(action)) {
      // create form
      ProductForm productForm = new ProductForm();
      // populate action properties
      productForm.setName(request.getParameter("name"));
      productForm.setDescription(
          request.getParameter("description"));
      productForm.setPrice(request.getParameter("price"));

      // create model
      Product product = new Product();
      product.setName(productForm.getName());
      product.setDescription(product.getDescription());
      try {
        product.setPrice(new BigDecimal(productForm.getPrice()));
      } catch (NumberFormatException e) {
      }
      // execute action method
      SaveProductAction saveProductAction = 
           new SaveProductAction();
      saveProductAction.save(product); 
      // store model in a scope variable for the view
      request.setAttribute("product", product);
      dispatchUrl = "/jsp/ProductDetails.jsp"; 
    } 
    // forward to a view
    if (dispatchUrl != null) { 
      RequestDispatcher rd = request 
          .getRequestDispatcher(dispatchUrl); 
      rd.forward(request, response);
    } else {
      // let static contents pass 
      filterChain.doFilter(request, response);
    } 
  }
}

doFilter方法的内容同appdesign1中process方法。

由于过滤器的过滤目标是包括静态内容在内的所有网址,因此,若没有相应的action则需要调用filterChain.doFilter()。

    } else {
      // let static contents pass 
      filterChain.doFilter(request, response);
    }

要测试应用,可以用浏览器访问如下URL:

http://localhost:8080/appdesign2/input-product

在Web应用执行action时,很重要的一个步骤就是进行输入校验。校验的内容可以是简单的,如检查一个输入是否为空,也可以是复杂的,如校验信用卡号。实际上,因为校验工作如此重要,Java社区专门发布了JSR 303 Bean Validation以及JSR 349 Bean Validation 1.1版本,将Java世界的输入检验进行标准化。现代的MVC框架通常同时支持编程式和声明式两种校验方法。在编程式中,需要通过编码进行用户输入校验,而在声明式中,则需要提供包含校验规则的XML文档或者属性文件。

注意

 

即使您可以使用HTML5或JavaScript执行客户端输入验证,也不要依赖它,因为精明的用户可以轻松地绕过它。始终执行服务器端输入验证!

本节的新应用(appdesign3)扩展自appdesign1,但多了一个ProductValidator类(见清单2.8)。

清单2.8 ProductValidator类

package appdesign3.validator;
import java.util.ArrayList;
import java.util.List;
import appdesign3.form.ProductForm;

public class ProductValidator {
  public List<String> validate(ProductForm productForm) {
    List<String> errors = new ArrayList< >();
    String name = productForm.getName();
    if (name == null || name.trim().isEmpty()) {
      errors.add("Product must have a name");
    }
    String price = productForm.getPrice();
    if (price == null || price.trim().isEmpty()) {
      errors.add("Product must have a price");
    } else {
      try {
        Float.parseFloat(price);
      } catch (NumberFormatException e) {
        errors.add("Invalid price value");
      }
    }
    return errors;
  }
}

注意

 

ProductValidator类中有一个操作ProductForm对象的validate方法,确保产品的名字非空,其价格是一个合理的数字。validate方法返回一个包含错误信息的字符串列表,若返回一个空列表,则表示输入合法。

现在需要让控制器使用这个校验器了,清单2.9展示了一个更新后的ControllerServlet,注意黑体部分。

清单2.9 新版的ControllerServlet类

package appdesign3.controller;
import java.io.IOException;
import java.util.List;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import appdesign3.action.SaveProductAction;
import appdesign3.form.ProductForm;
import appdesign3.model.Product;
import appdesign3.validator.ProductValidator;
import java.math.BigDecimal;

@WebServlet(name = "ControllerServlet", urlPatterns = {
    "/input-product", "/save-product"})
public class ControllerServlet extends HttpServlet {

  private static final long serialVersionUID = 98279L;
  @Override
  public void doGet(HttpServletRequest request, 
      HttpServletResponse response) 
      throws IOException, ServletException {
    process(request, response);
  }

  @Override
  public void doPost(HttpServletRequest request, 
      HttpServletResponse response) 
      throws IOException, ServletException {
    process(request, response);
  }

  private void process(HttpServletRequest request, 
      HttpServletResponse response) 
      throws IOException, ServletException {

    String uri = request.getRequestURI();
    /*
     * uri is in this form: /contextName/resourceName,
     * for example: /appdesign1/input-product.
     * However, in the case of a default context, the
     * context name is empty, and uri has this form
     * /resourceName, e.g.: /input-product
     */
    int lastIndex = uri.lastIndexOf("/");
    String action = uri.substring(lastIndex + 1);
    String dispatchUrl = null;

    if ("input-product".eauals(action)) {
      // no action class, Hrele is nathing to be done
      dispatchUrl = "/jsp/ProductForm.jsp";
    } else if ("save-product"-eaoals(action)) {
      // instantiatle action class
      ProductForm productForm = new ProductForm();
      // populate action properties
      productForm.setName(
          request.getParameter("name"));
      productForm.setDescription(
          request.getParameter("description"));
      productForm.setPrice(request.getParameter("price"));

      // validate ProductForm
      ProductValidator productValidator = new 
          ProductValidator();
      List< String> errors = 
          productValidator.validate(productForm);
      if(errors.isEmpty()){
        // create product from productForm
        Product product = new Product();
        product.setName(productForm.getName());
        product.setDescription(
            productForm.getDescription());
        product.setPrice(new BigDecimal (productForm.getPrice()));

        // no validation error execute action method
        SaveProductAction saveProductAction = new
            SaveProductAction();
        saveProductAction.save(product);
        // store model in a scope variable for the view
        request.setAttribute("product", product);
        dispatchUrl = "/jsp/ProductDetails.jsp"; 
      } else {
        request.setAttribute("errors", errors);
        request.setAttribute("form", productForm);
        dispatchUrl = "/jsp/ProductForm.jsp"; 
      } 
    }
    // forward to a new
    if (dispatchUrl != null) {
      RequestDispatcher rd = 
          request.getRequestDispatcher(dispatchUrl);
      rd.forward(request, response);
    }
  }
}

新版的ControllerServlet类添加了初始化ProductValidator类并调用其validate方法的代码。

    // validate ProductForm
    ProductValidator productValidator = new
        ProductValidator();
    List<String> errors =
        productValidator.validate(productForm);

validate方法接受一个ProductForm参数,它封装了输入到HTML表单的产品信息。如果不用ProductForm,则应将ServletRequest传递给验证器。

如果验证成功,validate方法返回一个空列表,在这种情况下,将创建一个产品并传递给SaveProductAction,然后,控制器将Product存储在ServletContext中,并转发到ProductDetails.jsp页面,显示产品的详细信息。如果验证失败,控制器将错误列表和ProductForm存储在ServletContext中,并返回到ProductForm.jsp。

if (errors.isEmpty()) {
    // create Product from ProductForm
    Product product = new Product();
    product.setName(productForm.getName());
    product.setDescription(
        productForm.getDescription());
    product.setPrice(new BigDecimal(productForm.getPrice()));
    // no validation error, execute action method
    SaveProductAction saveProductAction = new
        SaveProductAction();
    saveProductAction.save(product);
    // store action in a scope variable for the view
    request.setAttribute("product", product);
    dispatchur1="/jsp/ProductDetails.jsp";
  } else {
    request.setAttribute("errors", errors);
    request.setAttribute("form", productForm);
    dispatchur1="/jsp/ProductForm.jsp";
  }

现在,需要修改appdesign3应用的ProductForm.jsp页面(见清单2.10),使其可以显示错误信息以及错误的输入。

清单2.10 ProductForm.jsp页面

< !DOCTYPE html>
< html>
< head>
< title>Add Product Form< /title>
< style type="text/css">@import url(css/main.css);< /style>
< /head>
< body>
< form method="post" action="save-product"> 
  < h1>Add Product
    < span>Please use this form to enter product details< /span> 
  < /h1> 
  ${empty requestScope.errors? "" : "< p style='color:red'>"
   += "Error(s)!"
   += "< ul>"} 
  < !--${requestScope.errors.stream().map(
     x -> "-->< li>"+=x+="< /li>< !--").toList()}--> 
  ${empty requestScope.errors? "" : "< /ul>< /p>"} 
  < label>
    < span>Product Name :< /span>
    < input id="name" type="text" name="name" 
      placeholder="The complete product name" 
      value="${form.name}"/> 
  < /label>
  < label> 
    < span>Description :< /span>
    < input id="description" type="text" name="description" 
      placeholder="Product description" 
      value="${form.description}"/> 
  < /label>
  < label> 
    < span>Price :< /span>
    < input id="price" name="price" type="number" step="any" 
      placeholder="Product price in #.## format" 
      value="${form.price}"/> 
  < /label>
  < label> 
    < span>&nbsp;< /span> 
    < input type="submit"/>
  < /label> 
< /form>
< /body>
< /html>

现在访问input-product,测试appdesign3应用。

http://localhost:8080/appdesgin3/input-product

若产品表单提交了无效数据,页面将显示相应的错误信息。图2.6显示了包含两条错误信息的ProductForm页面。

图2.6 包含两条错误信息的ProductForm页面

在过去数年间,依赖注入技术作为代码可测试性的一个解决方案已经广泛应用。实际上,Spring、Struts2等伟大框架都采用了依赖注入技术。那么,什么是依赖注入技术?

关于这个,Martin Fowler写一篇优秀的文章:

http://martinfowler.com/articles/injection.html

在Fowler创造术语“依赖注入”之前,术语“控制反转”通常用于表示同样的事情。 正如Fowler在他的文章中指出的,两者不完全相同。

有两个组件A和B,A依赖于B。假定A是一个类,且A有一个方法importantMethod使用到了B,如下:

public class A {
  public void importantMethod() {
    B b = ... // get an instance of B
    b.usefulMethod();
    ...
  }
  ...
}

要使用B,类A必须先获得组件B的实例引用。若B是一个具体类,则可通过new关键字直接创建组件B实例。但是,如果B是接口,且有多个实现,则问题就变得复杂了。我们固然可以任意选择接口B的一个实现类,但这也意味着A的可重用性大大降低了,因为无法采用B的其他实现。

示例appdesign4使用了一个自制依赖注入器。在现实世界的应用程序中,应该使用Spring。

示例应用程序用来生成PDF。它有两个动作,form和pdf。 第一个没有action类,只是转发到可以用来输入一些文本的表单;第二个生成PDF文件并使用PDFAction类,操作类本身依赖于生成PDF的服务类。

PDFAction和PDFService类分别见清单2.11和清单2.12。

清单2.11 PDFAction类

package action;
import service.PDFService; 
public class PDFAction {
  private PDFService pdfService; 

  public void setPDFService(PDFService pdfService) {
    this.pdfService = pdfService; 
  } 
  public void createPDF(String path, String input) {
    pdfService.createPDF(path, input); 
  }
}

清单2.12 PDFService类

package service; 
import util.PDFUtil; 

public class PDFService {
  public void createPDF(String path, String input) { 
    PDFUtil.createDocument(path, input);
  } 
}

PDFService使用了PDFUtil类,PDFUtil最终采用了Apache PDFBOx库来创建PDF文档,如果对创建PDF的具体代码有兴趣,可以进一步查看PDFUtil类。

这里的关键在于,如代码2.11所示,PDFAction需要一个PDFService来完成它的工作。换句话说,PDFAction依赖于PDFService。没有依赖注入,你必须在PDFAction类中实例化PDFService类,这将使PDFAction更不可测试。除此之外,如果需要更改PDFService的实现,你必须重新编译PDFAction。

使用依赖注入,每个组件都有注入它的依赖项,这使得测试每个组件更容易。对于在依赖注入环境中使用的类,你必须使其支持注入。一种方法是为每个依赖关系创建一个set方法。例如,PDFAction类有一个setPDFService方法,可以调用它来传递PDFService。注入也可以通过构造方法或类属性进行。

一旦所有的类都支持注入,你可以选择一个依赖注入框架并将它导入你的项目。Spring框架、Google Guice、Weld和PicoContainer是一些好的选择。

注意

 

依赖注入的Java规范是JSR 330和JSR 299

appdesign4程序使用DependencyInjector类(见清单2.13)来替代依赖注入框架(在现实世界的应用程序中,你会使用一个合适的框架)。这个类专为appdesign4应用设计,可以容易地实例化。一旦实例化,必须调用其start方法来执行初始化,使用后,应调用其shutdown方法以释放资源。在此示例中,start和shutdown都为空。

清单2.13 DependencyInjector类

package util;
import action.PDFAction; 
import service.PDFService; 

public class DependencyInjector {

  public void start() { 
    // initialization code 
  } 
  public void shutDown() {
    // clean-up code 
  } 

  /* 
   * Returns an instance of type. type is of type Class 
   * and not String because it's easy to misspell a class name 
   */ 
  public Object getObject(Class type) { 
    if (type == PDFService.class) { 
      return new PDFService();
    } else if (type == PDFAction.class) { 
      PDFService pdfService = (PDFService)
          getObject(PDFService.class);
      PDFAction action = new PDFAction();
      action.setPDFService(pdfService);
      return action;
    } 
    return null;
  } 
}

要从DependencyInjector获取对象,须调用其getObject方法,并传递目标对象的Class。 DependencyInjector支持两种类型,即PDFAction和PDFService。例如,要获取PDFAction的实例,你将通过传递PDFAction.class来调用getObject:

PDFAction pdfAction =(PDFAction)dependencyInjector.getObject(PDFAction.class);

DependencyInjector(和所有依赖注入框架)的优雅之处在于它返回的对象注入了依赖。如果返回的对象所依赖的对象也有依赖,则所依赖的对象也会注入其自身的依赖。例如,从DependencyInjector获取的PDFAction已包含PDFService。无需在PDFAction类中自己创建PDFService。

appdesign4中的servlet控制器如清单2.14所示。请注意,它在其init方法中实例化DependencyInjector,并在其destroy方法中调用DependencyInjector的shutdown方法。 servlet不再创建它自己的依赖,相反,它从DependencyInjector获取这些依赖。

清单2.14 appdesign4中ControllerServlet

package servlet;
import action.PDFAction;
import java.io.IOException;
import javax.servlet.ReadListener;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException; 
import javax.servlet.annotation.WebServlet; 
import javax.servlet.http.HttpServlet; 
import javax.servlet.http.HttpServletRequest; 
import javax.servlet.http.HttpServletResponse; 
import javax.servlet.http.HttpSession;
import util.DependencyInjector; 

@WebServlet(name = "ControllerServlet", urlPatterns = { 
  "/form", "/pdf"}) 
public class ControllerServlet extends HttpServlet { 
private static final long serialVersionUID = 6679L; 
  private DependencyInjector dependencyInjector; 

  @Override
  public void init() { 
    dependencyInjector = new DependencyInjector(); 
    dependencyInjector.start();
  } 

  @Override
  public void destroy() {
    dependencyInjector.shutDown(); 
  } 
  protected void process(HttpServletRequest request, 
      HttpServletResponse response) 
      throws ServletException, IOException {
    ReadListener r = null;
    String uri = request.getRequestURI();
    /* 
     * uri is in this form: /contextName/resourceName, 
     * for example: /app10a/product_input. 
     * However, in the case of a default context, the 
     * context name is empty, and uri has this form 
     * /resourceName, e.g.: /pdf 
     */
    int lastIndex = uri.lastIndexOf("/");
    String action = uri.substring(lastIndex + 1);
    if ("form".equals(action)) {
      String dispatchUrl = "/jsp/Form.jsp"; 
      RequestDispatcher rd = 
          request.getRequestDispatcher(dispatchUrl);
      rd.forward(request, response); 
    } else if ("pdf".equals(action)) {
      HttpSession session = request.getSession(true); 
      String sessionId = session.getId();
      PDFAction pdfAction = (PDFAction) dependencyInjector 
          .getObject(PDFAction.class);
      String text = request.getParameter("text");
      String path = request.getServletContext() 
          .getRealPath("/result") + sessionId + ".pdf";
      pdfAction.createPDF(path, text); 

      // redirect to the new pdf 
      StringBuilder redirect = new 
          StringBuilder(); 
      redirect.append(request.getScheme() + "://"); 
      redirect.append(request.getLocalName());
      int port = request.getLocalPort();
      if (port != 80) { 
        redirect.append(":" + port); 
      } 
      String contextPath = request.getContextPath();
      if (!"/".equals(contextPath)) { 
        redirect.append(contextPath); 
      } 
      redirect.append("/result/" + sessionId + ".pdf"); 
      response.sendRedirect(redirect.toString()); 
    } 
  } 

  @Override
  protected void doGet(HttpServletRequest request, 
      HttpServletResponse response) 
      throws ServletException, IOException { 
    process(request, response); 
  } 

  @Override
  protected void doPost(HttpServletRequest request, 
      HttpServletResponse response) 
      throws ServletException, IOException { 
    process(request, response); 
  }
}

servlet支持两种URL模式,form和pdf。 对于表单模式,servlet简单地转发到表单。 对于pdf模式,servlet使用PDFAction并调用其createDocument方法。此方法有两个参数:文件路径和文本输入。所有PDF存储在应用程序目录下的result目录中,用户的会话标识符用做文件名,而文本输入作为PDF文件的内容;最后,重定向到生成的PDF文件。以下是创建重定向URL并将浏览器重定向到新URL的代码:

// redirect to the new pdf
StringBuilder redirect = new 
    StringBuilder();
redirect.append(request.getScheme() + "://"); //http or https 
redirect. append(request.getLocalName()); // the domain
int port = request.getLocalPort();
if (port != 80) { 
  redirect.append(":" + port); 
} 
String contextPath = request.getContextPath();
if (!"/".equals(contextPath)) { 
  redirect.append(contextPath); 
} 
redirect.append("/result/" + sessionId + ".pdf"); 
response.sendRedirect(redirect.toString());

现在访问如下URL来测试appdesign4应用。

http://localhost:8080/appdesign4/form

应用将展示一个表单(见图2.7)。

图2.7 PDF表单

如果在文本字段中输入一些内容并按提交按钮,服务器将创建一个PDF文件并发送重定向到浏览器(见图2.8)。

图2.8 PDF文件

请注意,重定向网址将采用此格式。

http://localhost:8080/appdesign4/result/sessionId.pdf

由于依赖注入器,appdesign4中的每个组件都可以独立测试。例如,可以运行清单2.15中的PDFActionTest类来测试类的createDocument方法。

清单2.15 PDFActionTest类

package test;
import action.PDFAction; 
import util.DependencyInjector; 

public class PDFActionTest {
  public static void main(String[] args) { 
    DependencyInjector dependencyInjector = new DependencyInjector();
    dependencyInjector.start();
    PDFAction pdfAction = (PDFAction) dependencyInjector.getObject(
        PDFAction.class); 
    pdfAction.createPDF("/home/janeexample/Downloads/1.pdf", 
        "Testing PDFAction...."); 
    dependencyInjector.shutDown(); 
  } 
}

如果你使用的是Java 7 EE容器,如Glassfish,可以让容器注入对servlet的依赖。 应用appdesign4中的servlet将如下所示:

public class ControllerServlet extends HttpServlet {
  @Inject PDFAction pdfAction;
  ... 

  @Override
  public void doGet(HttpServletRequest request, 
      HttpServletResponse response) throws IOException, 
      ServletException {
    ... 
  } 

  @Override
  public void doPost(HttpServletRequest request, 
      HttpServletResponse response) throws IOException, 
      ServletException {
    ... 
  }
}

在本章中,我们学习了基于MVC模式的模型2架构以及如何基于servlet控制器或者filter分发器开发一个模型2应用。两个示例分别为appdesign1和appdesign2。使用servlet作为过滤器上的控制器,一个明显的优点是你可以将servlet配置为欢迎页面。 在模型2应用程序中,JSP页面通常用做视图,当然也可以使用其他技术,如Apache Velocity和FreeMarker。 如果JSP页面用做模型2体系结构中的视图,那些页面仅用于显示值,并且不应在其中显示脚本元素。

在本章中,我们还构建了一个简单的MVC框架,其中包含一个验证器并为其配备了一个依赖注入器。虽然自制的框架是一个良好的学习工具,但未来的MVC项目应基于一个成熟的MVC框架上,如Spring MVC,而不是试图重复造轮子。


相关图书

深入浅出Spring Boot 3.x
深入浅出Spring Boot 3.x
云原生Spring实战Spring Boot与?Kubernetes实践
云原生Spring实战Spring Boot与?Kubernetes实践
Spring实战(第6版)
Spring实战(第6版)
Java研发自测入门与进阶
Java研发自测入门与进阶
Spring核心技术和案例实战
Spring核心技术和案例实战
Java EE企业级应用开发实战(Spring Boot+Vue+Element)
Java EE企业级应用开发实战(Spring Boot+Vue+Element)

相关文章

相关课程