书名:Spring MVC学习指南
ISBN:978-7-115-38639-7
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
• 著 [美] Paul Deck
译 林仪明 崔 毅
责任编辑 陈冀康
• 人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
• 读者服务热线:(010)81055410
反盗版热线:(010)81055315
Spring MVC是Spring框架中用于Web应用快速开发的一个模块,被广泛用作当今业界最主流的Web开发框架。
本书重在讲述如何通过Spring MVC来开发基于Java的Web应用。全书共计12章,分别从Spring框架、模型2和MVC模式、Spring MVC介绍、控制器、数据绑定和表单标签库、转换器和格式化、验证器、表达式语言、JSTL、国际化、上传文件、下载文件多个角度介绍了Spring MVC。除此之外,本书还配有丰富的示例以供读者练习和参考。
本书是一本Spring MVC的教程,内容细致、讲解清晰,非常适合Web开发者和想要使用Spring MVC开发基于Java的Web应用的读者阅读。
Simplified Chinese translation copyright © 2015 by Posts and Telecommunications Press
ALL RIGHTS RESERVED
Spring MVC A Tutorial by Paul Deck
Copyright © 2014 by Brainy Software Inc.
本书中文简体版由作者Paul Deck授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式或任何手段复制和传播。
版权所有,侵权必究。
林仪明,男,现为IBM高级工程师。2004年毕业于厦门大学软件学院,主要研究软件架构、应用中间件。目前在福州生活和工作,先后从事软件技术开发,软件架构设计以及团队管理等工作,有多年的开发设计和管理经验,目前提供IBM中间件产品支持工作。
崔毅,男,JustCommodity Software Solution Pte Ltd技术部经理。2007年毕业于北京航空航天大学计算机学院,获硕士学位,主要研究Web服务、信息交换中间件。目前在新加坡生活和工作,先后从事技术开发,系统分析,系统实施,咨询顾问,和产品研发管理等工作,有多年的开发设计和管理经验,目前负责一个产品线。
Spring MVC是Spring框架中用于Web应用快速开发的一个模块。Spring MVC的MVC是Model-View-Controller的缩写。它是一个广泛应用在图形化用户交互开发中的设计模式,不仅常见于Web开发,也广泛应用于桌面开发,如Java Swing。
作为当今业界最主流的Web开发框架,Spring MVC(有时也称Spring Web MVC)的开发技能相当热门。本书可供想要学习如何通过Spring MVC开发基于Java的Web应用的开发人员阅读。
Spring MVC基于Spring框架、Servlet和JSP(JavaServer Page),在掌握这3种技术的基础上学习Spring MVC将非常容易。本书第1章针对Spring新手提供一个快速教程,附录B和附录C将帮助你快速学习Servlet和JSP。如果希望深入学习Servlet和JSP,推荐阅读由Budi Kurniawan所著的Servlet and JSP:A Tutorial一书。
本章接下来将讨论HTTP、基于Servlet和JSP的Web编程,以及本书的章节结构。
HTTP使得Web服务器与浏览器之间可以通过互联网或内网进行数据交互。作为一个制定标准的国际社区,万维网联盟(W3C)负责和维护HTTP。HTTP第一版是0.9,之后是HTTP 1.0,当前最新版本是HTTP 1.1。HTTP 1.1版本的 RFC编号是2616,下载地址为http://www.w3.org/Protocols/HTTP/1.1/rfc2616.pdf。
Web服务器每天24小时不间断运行,并等待HTTP客户端(通常是Web浏览器)来连接并请求资源。通常,客户端发起一个连接,服务端不会主动连接客户端。
2011年,标准化组织IETF(因特网工程任务组)发布了WebSocket协议,即RFC 6455规范。该协议允许一个HTTP连接升级为WebSocket连接,支持双向通信,这就使得服务端可以通过WebSocket协议主动发起同客户端的会话通信。
互联网用户需要通过点击或者输入一个URL链接或地址来访问一个资源,如下为两个示例:
http://google.com/index.html
http://facebook.com/index.html
URL的第一个部分是HTTP,代表所采用的协议。除HTTP外,URL还可以采用其他类型的协议,如下为两个示例:
mailto:joe@example.com
ftp://marketing@ftp.example.com
通常,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请求包含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.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为响应第一次请求而被创建后,会驻留在内存中,以便响应后续请求。
JSP(JavaServer Pages)技术于1999年发布,以便简化Servlet开发。
从Servlet技术出现的那天起,人们开发了大量的Web框架来帮助程序员快速编写Web应用程序。这些开发框架让开发人员能更关注业务逻辑,减少编写“相似”的代码片段。尽管如此,开发人员依然需要去理解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》第3版(中文翻译版《Java 7程序设计》已由机械工业出版社出版)。
一个Servlet是一个Java程序,一个Servlet应用包含了一个或多个Servlet,一个JSP页面会被翻译并编译成一个Servlet。
一个Servlet应用运行在一个Servlet容器中,它无法独立运行。Servlet容器将来自用户的请求传递给Servlet应用,并将Servlet应用的响应返回给用户。由于大部分Servlet应用都会包含一些JSP界面,故称Java Web应用为“Servlet/JSP”应用会更恰当些。
Web用户通过一个诸如IE、火狐或者Chrome等Web浏览器来访问Servlet应用。Web浏览器又被称为Web客户端。下图展示了一个典型的Servlet/JSP应用架构。
图 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.org以及http://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。
第1章:Spring框架,介绍了最流行的开源框架。
第2章:模型2和MVC模式,讨论了Spring MVC实现的设计模式。
第3章:Spring MVC介绍,本章编写了第一个Spring MVC应用。
第4章:基于注解的控制器,讨论了MVC模式中最重要的一个对象——控制器。本章,我们将学习如何编写基于注解的控制器,该方式由Spring MVC 2.5版本引入。
第5章:数据绑定和表单标签库,讨论Spring MVC最强大的一个特性,并利用它来展示表单数据。
第6章:转换器和格式化,讨论了数据绑定的辅助对象类型。
第7章:验证器,展示如何通过验证器来验证用户输入数据。
第8章:表达式语言。
第9章:JSTL。
第10章:国际化。
第11章:上传文件。
第12章:下载文件。
附录A:Tomcat。
附录B:Servlet。
附录C:JavaServer Pages。
附录D:部署描述符。
本书所有的示例应用压缩包可以通过如下地址下载:
http://books.brainysoftware.com/download
Spring框架是一个开源的企业应用开发框架,作为一个轻量级的解决方案,其包含20多个不同的模块。本书主要关注Core和Bean,以及Spring MVC模块。Spring MVC是Spring的一个子框架,也是本书的主题。
本章主要介绍Core和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文件。配置文件的根元素通常为:
<?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
<a>http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"></a>
...
</beans>
如果需要更强的Spring配置能力,可以在schema location属性中添加相应的schema。配置文件可以是一份,也可以分解为多份,以支持模块化配置。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
<a>http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"></a>
<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
<a>http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"></a>
<bean name="product" class="app01a.bean.Product"/>
</beans>
该bean的定义告诉Spring通过默认无参的构造器来初始化Product类。如果不存在该构造器(如果类作者重载了构造器,且没有显示声明默认构造器),则Spring将抛出一个异常。
注意,应采用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.util.Calendar。
<bean id="calendar" class="java.util.Calendar"
factory-method="getInstance"/>
本例中采用了id属性,而非name属性来标识bean,采用了getBean方法来获取Calendar实例。
ApplicationContext context =
new ClassPathXmlApplicationContext(
new String[] {"spring-config.xml"});
Calendar calendar = context.getBean("calendar", Calendar.class);
有时,我们希望一些类在被销毁前能执行一些方法。Spring考虑到了这样的需求。可以在bean定义中配置destroy-method属性,来指定在销毁前要被执行的方法。
下面的例子中,我们配置Spring通过java.util.concurrent.Executors的静态方法newCachedThreadPool来创建一个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 app01a.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="app01a.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="app01a.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 app01a.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 app01a.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="app01a.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="app01a.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="app01a.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="app01a.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,并展示3个不同示例应用。第一个应用是一个基本的模型2应用,采用Servlet作为控制器,第二个应用引入了控制器,第三个应用引入了验证控件来校验用户的输入。
第一次学习JSP,通常通过链接方式进行JSP页面间的跳转。这种方式非常直接,但在中型和大型应用中,这种方式会带来维护上的问题。修改一个JSP页面的名字,会导致大量页面中的链接需要修正。因此,实践中并不推荐模型1(但仅2~3个页面的应用除外)。
模型2基于模型—视图—控制器(MVC)模式,该模式是Smalltalk-80用户交互的核心概念,那时还没有设计模式的说法,当时称为MVC范式。
一个实现MVC模式的应用包含模型、视图和控制器3个模块。视图负责应用的展示。模型封装了应用的数据和业务逻辑。控制器负责接收用户输入,改变模型,以及调整视图的显示。
注:
Steve Burbeck博士的论文:Applications Programming in Smalltalk-80(TM):How to use Model-View-Controller (MVC)详细讨论了MVC模式,论文地址为http://st-www.cs.illinois.edu/ users/smarch/st-docs/mvc.html。
模型2中,Servlet或者Filter都可以充当控制器。几乎所有现代Web框架都是模型2的实现。Spring MVC和Struts 1使用一个Servlet作为控制器,而Struts 2则使用一个Filter作为控制器。大部分都采用JSP页面作为应用的视图,当然也有其他技术。而模型则采用POJO(Plain Old Java Object)。不同于EJB等,POJO是一个普通对象。实践中会采用一个JavaBean来持有模型状态,并将业务逻辑放到一个Action类中。一个JavaBean必须拥有一个无参的构造器,通过get/set方法来访问参数,同时支持持久化。
图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/product_input
通过类似如下URI,来保存产品。
http://domain/appName/product_save
控制器会解析URI并调用相应的action,然后将模型对象放到视图可以访问的区域(以便服务端数据可以展示在浏览器上)。最后,控制器利用RequestDispatcher跳转到视图(JSP页面)。在JSP页面中,用表达式语言以及定制标签显示数据。
注意:调用RequestDispatcher.forward方法并不会停止执行剩余的代码。因此,若forward方法不是最后一行代码,则应显式地返回。
为了便于对模型2有一个直观的了解,本节将展示一个简单模型2应用。实践中,模型2应用非常复杂。
示例应用名为app02a,其功能设定为输入一个产品信息。具体为:用户填写产品表单(图2.2)并提交;示例应用保存产品并展示一个完成页面,显示已保存的产品信息(图2.3)。
图2.2 产品表单
示例应用支持如下两个action。
(1)展示“添加产品”表单。该action发送图2.2中的输入表单到浏览器上,其对应的URI应包含字符串product_input。
(2)保存产品并返回图2.3所示的完成页面,对应的URI必须包含字符串product_save。
示例应用app02a由如下组件构成。
(1)一个Product类,作为product的领域对象。
图2.3 产品详细页
(2)一个ProductForm类,封装了HTML表单的输入项。
(3)一个ControllerServlet类,本示例应用的控制器。
(4)一个SaveProductAction类。
(5)两个JSP页面(ProductForm.jsp和ProductDetail.jsp)作为view。
(6)一个CSS文件,定义了两个JSP页面的显示风格。
app02a目录结构如图2.4所示。
图2.4 app02a目录结构
所有的JSP文件都放置在WEB-INF目录下,因此无法被直接访问。下面详细介绍示例应用的每个组件。
Product实例是一个封装了产品信息的JavaBean。Product类(见清单2.1)包含3个属性:productName、description和price。
清单2.1 Product类
package app02a.domain;
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 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;
}
}
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 app02a.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 app02a.servlet;
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import app02a.domain.Product;
import app02a.form.ProductForm;
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: /app10a/product_input.
* However, in the event of a default context, the
* context name is empty, and uri has this form
* /resourceName, e.g.: /product_input
*/
int lastIndex = uri.lastIndexOf("/");
String action = uri.substring(lastIndex + 1);
// execute an action
if (action.equals("product_input.action")) {
// no action class, there is nothing to be done
} else if (action.equals("product_save.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(Float.parseFloat(
productForm.getPrice()));
} catch (NumberFormatException e) {
}
// code to save product
// store model in a scope variable for the view
request.setAttribute("product", product);
}
// forward to a view
String dispatchUrl = null;
if (action.equals("product_input.action")) {
dispatchUrl = "/WEB-INF/jsp/ProductForm.jsp";
} else if (action.equals("product_save.action")) {
dispatchUrl = "/WEB-INF/jsp/ProductDetails.jsp";
}
if (dispatchUrl != null) {
RequestDispatcher rd =
request.getRequestDispatcher(dispatchUrl);
rd.forward(request, response);
}
}
}
若基于Servlet 3.0规范,则可以采用注解的方式,而无需在部署描述符中进行映射。
...
import javax.servlet.annotation.WebServlet;
...
@WebServlet(name = "ControllerServlet", urlPatterns = {
"/product_input", "/product_save" })
public class ControllerServlet extends HttpServlet {
...
}
ControllerServlet的process方法处理所有输入请求。首先是获取请求URI和action名称。
String uri = request.getRequestURI();
int lastIndex = uri.lastIndexOf("/");
String action = uri.substring(lastIndex + 1);
在本示例应用中,action值只会是product_input或product_save。
接着,process方法执行如下步骤。
(1)创建并根据请求参数构建一个表单对象。product_save操作涉及3个属性:name、description和price。然后创建一个领域对象,并通过表单对象设置相应属性。
(2)执行针对领域对象的业务逻辑,包括将其持久化到数据库中。
(3)转发请求到视图(JSP页面)。
process方法中判断action的if代码块如下:
// execute an action
if (action.equals("product_input")) {
// there is nothing to be done
} else if (action.equals("product_save")) {
...
// code to save product
}
对于product_input,无需任何操作,而针对product_save,则创建一个ProductForm对象和Product对象,并将前者的属性值复制到后者。这个步骤中,针对空字符串的复制处理将留到稍后的“校验器”一节处理。
再次,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(product.getDescription());
try {
product.setPrice(Float.parseFloat(
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);
然后,将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
String dispatchUrl = null;
if (action.equals("Product_input")) {
dispatchUrl = "/WEB-INF/jsp/ProductForm.jsp";
} else if (action.equals("Product_save")) {
dispatchUrl = "/WEB-INF/jsp/ProductDetails.jsp";
}
if (dispatchUrl != null) {
RequestDispatcher rd = request.getRequestDispatcher(dispatchUrl);
rd.forward(request, response);
}
示例应用包含两个JSP页面。第一个页面ProductForm.jsp对应于product_input操作,第二个页面ProductDetails.jsp对应于product_save操作。ProductForm.jsp以及ProductDetails.jsp页面代码分别见清单 2.4和清单2.5。
清单2.4 ProductForm.jsp
<!DOCTYPE HTML>
<html>
<head>
<title>Add Product Form</title>
<style type="text/css">@import url(css/main.css);</style>
</head>
<body>
<div id="global">
<form action="product_save.action" method="post">
<fieldset>
<legend>Add a product</legend>
<p>
<label for="name">Product Name: </label>
<input type="text" id="name" name="name" tabindex="1">
</p>
<p>
<label for="description">Description: </label>
<input type="text" id="description" name="description" tabindex="2">
</p>
<p>
<label for="price">Price: </label>
<input type="text" id="price" name="price" tabindex="3">
</p>
<p id="buttons">
<input id="reset" type="reset" tabindex="4">
<input id="submit" type="submit" tabindex="5" value="Add Product">
</p>
</fieldset>
</form>
</div>
</body>
</html>
清单2.5 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表单。页面没有采用HTML表格方式进行布局,而采用了位于css目录下的main.css中的CSS样式表进行控制。
ProductDetails.jsp页面通过表达式语言(EL)访问HttpServletRequest所包含的product对象。本书第8章“表达式语言”会详细介绍。
本示例应用作为一个模型2的应用,可以通过如下几种方式避免用户通过浏览器直接访问JSP页面。
假定示例应用运行在本机的8080端口上,则可以通过如下URL访问应用:
http://localhost:8080/app02a/product_input.action
浏览器将显示图2.2的内容。
完成输入后,表单提交到如下服务端URL上:
http://localhost:8080/app02a/product_save.action
注意:可以将servlet控制器作为默认主页。这是一个非常重要的特性,使得在浏览器地址栏中仅输入域名(如http://example.com),就可以访问到该servlet控制器,这是无法通过filter方式完成的。
app02a中的业务逻辑代码都写在了Servlet控制器中,这个Servlet类将随着应用复杂度的增加而不断膨胀。为避免此问题,我们应该将业务逻辑代码提取到独立的被称为controller的类中。
在app02b应用(app02a的升级版)中,controller目录下有两个controller类,分别是InputProductController和SaveProductController。app02b应用的目录结构如图2.4所示。
图2.4 app02b应用的目录结构
这两个controller都实现了Controller接口(见清单2.6)。Controller接口只有handleRequest一个方法。Controller接口的实现类通过该方法访问到当前请求的HttpServletRequest和HttpServletResponse对象。
清单2.6 Controller接口
package app02b.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public interface Controller {
String handleRequest(HttpServletRequest request,
HttpServletResponse response);
}
InputProductController类(见清单2.7)直接返回了ProductForm.jsp的路径。而SaveProductController类(见清单2.8)则会读取请求参数来构造一个ProductForm对象,之后用ProductForm对象来构造一个Product对象,并返回ProductDetail.jsp路径。
清单2.7 InputProductController类
package app02b.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class InputProductController implements Controller {
@Override
public String handleRequest(HttpServletRequest request,
HttpServletResponse response) {
return "/WEB-INF/jsp/ProductForm.jsp";
}
}
清单2.8 SaveProductController类
package app02b.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import app02b.domain.Product;
import app02b.form.ProductForm;
public class SaveProductController implements Controller {
@Override
public String handleRequest(HttpServletRequest request,
HttpServletResponse response) {
ProductForm productForm = new ProductForm();
// populate form 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(Float.parseFloat(
productForm.getPrice()));
} catch (NumberFormatException e) {
}
// insert code to add product to the database
request.setAttribute("product", product);
return "/WEB-INF/jsp/ProductDetails.jsp";
}
}
将业务逻辑代码迁移到controller类的好处很明显:Controller Servlet变得更加专注。现在作用更像一个dispatcher,而非一个controller,因此,我们将其改名为DispatcherServlet。DispatcherServlet类(见清单2.9)检查每个URI,创建相应的controller,并调用其handleRequest方法。
清单2.9 DispatcherServlet类
package app02b.servlet;
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import app02b.controller.InputProductController;
import app02b.controller.SaveProductController;
public class DispatcherServlet extends HttpServlet {
private static final long serialVersionUID = 748495L;
@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: /app10a/product_input.
* However, in the event of a default context, the
* context name is empty, and uri has this form
* /resourceName, e.g.: /product_input
*/
int lastIndex = uri.lastIndexOf("/");
String action = uri.substring(lastIndex + 1);
String dispatchUrl = null;
if (action.equals("product_input.action")) {
InputProductController controller =
new InputProductController();
dispatchUrl = controller.handleRequest(request,
response);
} else if (action.equals("product_save.action")) {
SaveProductController controller =
new SaveProductController();
dispatchUrl = controller.handleRequest(request,
response);
}
if (dispatchUrl != null) {
RequestDispatcher rd =
request.getRequestDispatcher(dispatchUrl);
rd.forward(request, response);
}
}
}
现在,可以在浏览器中输入如下URL,测试应用了:
http://localhost:8080/app02b/product_input.action
在Web应用执行action时,很重要的一个步骤就是进行输入校验。校验的内容可以是简单的,如检查一个输入是否为空,也可以是复杂的,如校验信用卡号。实际上,因为校验工作如此重要,Java社区专门发布了JSR 303 Bean Validation以及JSR 349 Bean Validation 1.1版本,将Java世界的输入检验进行标准化。现代的MVC框架通常同时支持编程式和申明式两种校验方法。在编程式中,需要通过编码进行用户输入校验,而在申明式中,则需要提供包含校验规则的XML文档或者属性文件。
本节的新应用(app02c)扩展自app02b。图2.5展示了app02c的目录结构。
图2.5 app02c的目录结构
app02c应用的结构与app02b应用的结构基本相同,但多了一个ProductValidator类以及两个JSTL jar包(位于WEB-INF/lib目录下)。关于JSTL,将留到第9章“JSTL”中深入讨论。本节,我们仅需知道JSTL的作用是在ProductForm.jsp页面中展示输入校验的错误信息。
关于ProductValidator类,详见清单2.10。
清单2.10 ProductValidator类
package app02c.validator;
import java.util.ArrayList;
import java.util.List;
import app02c.form.ProductForm;
public class ProductValidator {
public List<String> validate(ProductForm productForm) {
List<String> errors = new ArrayList<String>();
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方法返回一个包含错误信息的字符串列表,若返回一个空列表,则表示输入合法。
应用中唯一需要用到产品校验的地方是保存产品时,即SaveProductController类。现在,我们为SaveProductController类引入ProductValidator类(见清单2.11)。
清单2.11 新版的SaveProductController类
package app02c.controller;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import app02c.domain.Product;
import app02c.form.ProductForm;
import app02c.validator.ProductValidator;
public class SaveProductController implements Controller {
@Override
public String handleRequest(HttpServletRequest request,
HttpServletResponse response) {
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(Float.parseFloat(
productForm.getPrice()));
// no validation error, execute action method
// insert code to save product to the database
// store product in a scope variable for the view
request.setAttribute("product", product);
return "/WEB-INF/jsp/ProductDetails.jsp";
} else {
//store errors and form in a scope variable for the view
request.setAttribute("errors", errors);
request.setAttribute("form", productForm);
return "/WEB-INF/jsp/ProductForm.jsp";
}
}
}
新版的SaveProductController类新增了初始化ProductValidator类,并调用其validate方法的代码。
// validate ProductForm
ProductValidator productValidator = new ProductValidator();
List<String> errors =
productValidator.validate(productForm);
如果校验发现有错误,则SaveProductController的handleRequest方法会转发到ProductForm. jsp页面。若没有错误,则创建一个Product对象,设置属性,并转到/WEB-INF/jsp/ ProductDetails.jsp页面。
if (errors.isEmpty()) {
// create Product from ProductForm
Product product = new Product();
product.setName(productForm.getName());
product.setDescription(productForm.getDescription());
product.setPrice(Float.parseFloat(
productForm.getPrice()));
// no validation error, execute action method
// insert code to save product to the database
// store product in a scope variable for the view
request.setAttribute("product", product);
return "/WEB-INF/jsp/ProductDetails.jsp";
} else {
//store errors and form in a scope variable for the view
request.setAttribute("errors", errors);
request.setAttribute("form", productForm);
return "/WEB-INF/jsp/ProductForm.jsp";
}
当然,实际应用中,这里会有把Product保存到数据库或者其他存储类型的代码,但现在我们仅关注输入校验。
现在,需要修改app02c应用的ProductForm.jsp页面(见清单2.12),使其可以显示错误信息以及错误的输入。
清单2.12 ProductForm.jsp页面
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE HTML>
<html>
<head>
<title>Add Product Form</title>
<style type="text/css">@import url(css/main.css);</style>
</head>
<body>
<div id="global">
<c:if test="${requestScope.errors != null}">
<p id="errors">
Error(s)!
<ul>
<c:forEach var="error" items="${requestScope.errors}">
<li>${error}</li>
</c:forEach>
</ul>
</p>
</c:if>
<form action="product_save.action" method="post">
<fieldset>
<legend>Add a product</legend>
<p>
<label for="name">Product Name: </label>
<input type="text" id="name" name="name"
tabindex="1">
</p>
<p>
<label for="description">Description: </label>
<input type="text" id="description"
name="description" tabindex="2">
</p>
<p>
<label for="price">Price: </label>
<input type="text" id="price" name="price"
tabindex="3">
</p>
<p id="buttons">
<input id="reset" type="reset" tabindex="4">
<input id="submit" type="submit" tabindex="5"
value="Add Product">
</p>
</fieldset>
</form>
</div>
</body>
</html>
现在访问product_input,测试app02c应用。
http://localhost:8080/app02c/product_input.action
若产品表单提交了非法数据,页面将显示相应的错误信息。图2.6显示了包含2条错误信息的ProductForm页面。
图2.6 包含两条错误信息的ProductForm页面
app02a、app02b和app02c应用都演示了如何进行前端处理。那么,后端处理呢?我们当然需要处理数据库等。
应用MVC,可以在Controller类中调用后端业务逻辑。通常,需要若干封装了后端复杂逻辑的Service类。在Service类中,可以实例化一个DAO类来访问数据库。在Spring环境中,Service对象可以自动被注入到Controller实例中,而DAO对象可以自动被注入到Service对象中,后续章节将有演示。
本章,我们学习了基于MVC模式的模型2架构以及如何开发一个模型2应用。在模型2应用中,JSP页面通常作为视图。当然,其他技术(如Apache Velocity或FreeMarker)也可以作为视图。若采用JSP页面作为视图,则这些页面仅用来展示数据,并且没有其他脚本元素。
本章,我们还构建了一个带校验器组件的简单MVC框架。