16.5 校验器

Web应用执行action时,很重要的一个步骤就是进行输入校验。校验的内容可以是简单的,如检查一个输入是否为空,也可以是复杂的,如校验信用卡号。实际上,因为校验工作如此重要,Java社区专门发布了JSR 303 Bean Validation以及JSR 349 Bean Validation 1.1版本,将Java的输入检验进行标准化。现代的MVC框架通常同时支持编程式和申明式两种校验方法。在编程式中,需要通过编码进行用户输入校验,而在声明式中,则需要提供包含校验规则的XML文档或者属性文件。
本节的新应用(app16c)扩展自app16b。图16.6展示了app16c的目录结构。
app16c应用的结构与app16b应用的结构基本相同,但多了一个ProductValidator类以及两个JSTL jar包(位于WEB-INF/lib目录下)。关于JSTL,将留到第9章“JSTL”中深入讨论。本节,我们仅需知道 JSTL 的作用是在ProductForm.jsp页面中显示输入校验的错误信息。
关于ProductValidator类,详见清单16.10。

ProductValidator类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package app16c.validator;
import java.util.ArrayList;
import java.util.List;
import app16c.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类。

新版的SaveProductController类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package app16b.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import app16b.domain.Product;
import app16b.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";
}
}

如果校验发现有错误,则SaveProductControllerhandleRequest方法会转发到ProductForm.jsp页面。若没有错误,则创建一个Product对象,设置属性,并转到/WEB-INF/jsp/ ProductDetails.jsp页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
        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保存到数据库或者其他存储类型的代码,但现在我们仅关注输入校验。
现在,需要修改app16c应用的ProductForm.jsp页面,使其可以显示错误信息以及错误的输入。

ProductForm.jsp页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<%@ 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,测试app16c应用:
http://localhost:8080/app16c/product_input.action
若产品表单提交了非法数据,页面将显示相应的错误信息。图16.7显示了包含2条错误信息的ProductForm页面。

16.4 解耦控制器代码

app16a中的业务逻辑代码都写在了Servlet控制器中,这个Servlet类将随着应用复杂度的增加而不断膨胀。为避免此问题,我们应该将业务逻辑代码提取到独立的被称为controller的类中
app16b应用(app16a的升级版)中,controller目录下有两个controller类,分别是InputProductControllerSaveProductControllerapp16b应用的目录结构如下所示:
这里有一张图
这两个controller都实现了Controller接口(见清单16.6)。Controller接口只有handleRequest一个方法。Controller接口的实现类通过该方法访问到当前请求的HttpServletRequestHttpServletResponse对象。

Controller接口

1
2
3
4
5
6
7
8
package app16b.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public interface Controller
{
String handleRequest(HttpServletRequest request,
HttpServletResponse response);
}

InputProductController类直接返回了ProductForm.jsp的路径。
SaveProductController类则会读取请求参数来构造一个ProductForm对象,之后用ProductForm对象来构造一个Product对象,并返回ProductDetail.jsp路径。

InputProductController类

1
2
3
4
5
6
7
8
9
10
package app16b.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";
}
}

SaveProductController类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package app16b.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import app16b.domain.Product;
import app16b.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类的好处很明显:ControllerServlet变得更加专注。现在作用更像一个dispatcher,而非一个controller,因此,我们将其改名为DispatcherServletDispatcherServlet类(见清单16.9)检查每个URI,创建相应的controller,并调用其handleRequest方法。

DispatcherServlet类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package app16b.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 app16b.controller.InputProductController;
import app16b.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/app16b/product_input.action

1.8 GenericServlet

通过实现Servlet接口方式来编写Servlet,你必须实现Servlet接口之中的所有方法,即便其中有一些方法是根本就没有包含任何代码的空方法。此外,还需要将ServletConfig对象保存到类级变量中。值得庆幸的是**GenericServlet抽象类的出现。本着尽可能使代码简单的原则,GenericServlet实现了ServletServletConfig接口**,并完成以下任务:
-将init方法中的ServletConfig赋给一个类级变量,以便可以通过调用getServletConfig获取。
-为Servlet接口中的所有方法提供默认的实现。
-提供方法,包围ServletConfig中的方法。

GenericServlet通过将ServletConfig赋给init方法中的servletConfig这个类级变量,来保存ServletConfig。下面就是GenericServlet中的init实现:

1
2
3
4
5
6
public void init(ServletConfig servletConfig) 
throws ServletException
{
this.servletConfig = servletConfig;
this.init();
}

Servlet中的init方法,并且还必须调用super.init(servletConfig)来保存ServletConfig。为了避免上述麻烦,**GenericServlet提供了第二个init方法**,它不带参数。这个方法是在ServletConfig被赋给servletConfig后,由第一个init方法调用:

1
2
3
4
5
6
public void init(ServletConfig servletConfig) 
throws ServletException
{
this.servletConfig = servletConfig;
this.init();
}

这意味着,可以通过覆盖没有参数的init方法来编写初始化代码,ServletConfig则仍然由GenericServlet实例保存。下面的GenericServletDemoServlet类是对之前的ServletConfigDemoServlet类的改写。注意,这个新的Servlet扩展(继承)了GenericServlet,而不是实现Servlet

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package app01a;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.GenericServlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
@WebServlet(
name = "GenericServletDemoServlet",
urlPatterns =
{"/generic"},
initParams =
{@WebInitParam(
name = "admin",
value = "Harry Taciak"
),@WebInitParam(
name = "email",
value = "admin@example.com"
)}
)
public class GenericServletDemoServlet
extends GenericServlet
{
private static final long serialVersionUID = 62500890L;
//只需要覆盖用用到的方法即可
@Override
public void service(ServletRequest request,
ServletResponse response)
throws ServletException,IOException
{
ServletConfig servletConfig = getServletConfig();
String admin = servletConfig.getInitParameter("admin");
String email = servletConfig.getInitParameter("email");
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
writer.print("<html><head></head><body>" +
"Admin:" + admin
+ "<br/>Email:" + email + "</body></html>");
}
}

可见,通过扩展(继承)GenericServlet方式编写的Servlet,不需要覆盖那些没计划要改变的方法。因此,代码变得更加整洁。在上述代码中,唯一被覆盖的方法是service方法。而且不必手动保存ServletConfig。 利用下面这个URL调用Servlet,其结果应该与 ServletConfigDemoServlet相似:
http://localhost:8080/Hello/generic
即使
GenericServlet是对Servlet一个很好的加强,但它也不常用
,因为它毕竟不像HttpServlet那么高级。**HttpServlet才是主角,在现实的应用程序中被广泛使用**。关于它的详情,请查阅1.9节。

16.3 模型2之Servlet控制器

为了便于对模型2有一个直观的了解,本节将展示一个简单模型2应用。实践中,模型2应用非常复杂。
示例应用名为app16a,其功能设定为输入一个产品信息。具体为:
用户填写产品表单(见图16.2)并提交;示例应用保存产品并展示一个完成页面,显示已保存的产品信息(见图16.3)。
示例应用支持如下两个action
(1)展示“添加产品”表单。该action发送图16.2中的输入表单到浏览器上,其对应的URI应包含字符串product_input
(2)保存产品并返回图16.3所示的完成页面,对应的URI必须包含字符串product_save
图16.3 产品详细页
示例应用app16a由如下组件构成:
(1)一个Product类,作为product的领域对象。
(2)一个ProductForm类,封装了HTML表单的输入项。
(3)一个ControllerServlet类,本示例应用的控制器。
(4)一个SaveProductAction类。
(5)两个JSP页面(ProductForm.jspProductDetail.jsp)作为view
(6)一个CSS文件,定义了两个JSP页面的显示风格。
app16a结构如图16.4所示。

图16.4 app16a目录结构

所有的JSP文件都放置在WEB-INF目录下,因此无法被直接访问。下面详细介绍示例应用的每个组件。

16.3.1Product类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package app16a.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属性。

16.3.2 ProductForm类

表单类与HTML表单相映射,是后者在服务端的代表。ProductForm类(见清单2.2)包含了一个产品的字符串值。ProductForm类看上去与Product类相似,这就引出一个问题:ProductForm类是否有存在的必要。
实际上,表单对象会传递ServletRequest给其他组件,类似Validator(本章后续段落会介绍)。而ServletRequest是一个Servlet层的对象,不应当暴露给应用的其他层。
另一个原因是,当数据校验失败时,表单对象将用于保存和展示用户在原始表单上的输入。16.5节将会详细介绍应如何处理。
注意:
大部分情况下,一个表单类不需要实现Serializable接口,因为表单对象很少保存在HttpSession中。

ProductForm类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package app16a.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;
}
}

16.3.3 ControllerServlet类

ControllerServlet类(见清单16.3)继承自javax.servlet.http.HttpServlet类,其doGetdoPost方法最终调用process方法,该方法是整个servlet控制器的核心。
可能有人好奇为何这个Servlet控制器被命名为ControllerServlet,实际上,这里遵从了一个约定:所有Servlet的类名称都带有Servlet后缀。

ControllerServlet类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package app16a.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 app16a.domain.Product;
import app16a.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规范,则可以采用注解的方式,而无须在部署描述符中进行映射:

1
2
3
4
5
6
7
8
...
import javax.servlet.annotation.WebServlet;
...
@WebServlet(name = "ControllerServlet", urlPatterns = {
"/product_input", "/product_save" })
public class ControllerServlet extends HttpServlet {
...
}

ControllerServletprocess方法处理所有输入请求。首先是获取请求URIaction名称:

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

在本示例应用中,action值只会是product_inputproduct_save
接着,process方法执行如下步骤:
(1)创建并根据请求参数构建一个表单对象。product_save操作涉及3个属性:namedescriptionprice。然后创建一个领域对象,并通过表单对象设置相应属性。
(2)执行针对领域对象的业务逻辑,包括将其持久化到数据库中。
(3)转发请求到视图(JSP页面)。
process方法中判断actionif代码块如下:

1
2
3
4
5
6
7
// 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方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 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对象中,以便对应的视图能访问到:

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

最后,process方法转到视图,如果actionproduct_input,则转到ProductForm.jsp页面,否则转到ProductDetails.jsp页面:

1
2
3
4
5
6
7
8
9
10
11
12
// 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);
}

16.3.4 视图

示例应用包含两个JSP页面。第一个页面ProductForm.jsp对应于product_input操作,第二个页面ProductDetails.jsp对应于product_save操作。ProductForm.jsp以及ProductDetails.jsp页面代码分别见清单16.4和清单16.5。

ProductForm.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!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>

ProductDetails.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!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页面:

  • JSP页面都放到WEB-INF目录下。WEB-INF目录下的任何文件或子目录都受保护,无法通过浏览器直接访问,但控制器依然可以转发请求到这些页面。
  • 利用一个servlet filter过滤JSP页面。
  • 在部署描述符中为JSP页面增加安全限制。这种方式相对容易些,无须编写 filter代码。

16.3.5 测试应用

假定示例应用运行在本机的8080端口上,则可以通过如下URL访问应用:
http://localhost:8080/app16a/product_input.action
浏览器将显示图16.2的内容。
完成输入后,表单提交到如下服务端URL上:

1
http://localhost:8080/app16a/product_save.action

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

16.2 模型2介绍

模型2基于模型-视图-控制器MVC)模式,该模式是Smalltalk-80用户交互的核心概念,那时还没有设计模式的说法,当时称为MVC范式。
一个实现MVC模式的应用包含模型视图控制器3个模块

  • 视图负责应用的展示。
  • 模型封装了应用的数据和业务逻辑。
  • 控制器负责接收用户输入、改变模型以及调整视图的显示。

模型2中,Servlet或者Filter都可以充当控制器。几乎所有现代Web框架都是模型2的实现。

  • Spring MVCStruts 1使用一个Servlet作为控制器,而Struts 2则使用一个Filter作为控制器
  • 大部分都采用JSP页面作为应用的视图,当然也有其他技术。
  • 而模型则采用POJOPlain Old Java Object)。

不同于EJB等,POJO是一个普通对象。实践中会采用一个JavaBean来持有模型状态,并将业务逻辑放到一个Action类中。一个JavaBean必须拥有一个无参的构造器,通过getter/setter方法来访问参数,同时支持持久化。

一个模型2应用的架构图如下图所示:

这里有一张图片

每个HTTP请求都发送给控制器,请求中的URI标识出对应的actionaction代表了应用可以执行的一个操作。一个提供了ActionJava对象称为action对象。一个action类可以支持多个actions或者一个action

看似简单的操作可能需要多个action。如,向数据库添加一个产品,需要两个action
(1)显示一个“添加产品”的表单,以便用户能输入产品信息。
(2)将表单信息保存到数据库中。
如前述,我们需要通过URI方式告诉控制器执行相应的action。例如,通过发送类似如下URI,来显示“添加产品”表单:

1
http://domain/appName/product_input

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

1
http://domain/appName/product_save

控制器会解析URI并调用相应的action,然后将模型对象放到视图可以访问的区域(以便服务端数据可以展示在浏览器上)。最后,控制器利用RequestDispatcher跳转到视图(JSP页面)。在JSP页面中,用表达式语言以及定制标签显示数据。
注意
调用RequestDispatcher.forward方法时,剩余的代码并不会停止执行。因此,若forward方法不是最后一行代码,则应显式地返回。

前言-3-本书内容简介

第一部分:Servlet和JSP

  • 第1章: “Servlets“,介绍Servlet API,本章重点关注两个java包:javax.servletjavax.servlet.http
  • 第2章: “会话管理”,讨论了会话管理——在Web应用开发中非常重要的主题(因为HTTP是无状态的),本章比较了4种不同的状态保持技术:URL重写、隐藏域、CookiesHTTPSession对象。
  • 第3章: “JavaServerPages(JSP)”,JSPServlet技术的补充完善,是Servlet技术的重要组成部分,本章包括了JSP语法、指令、脚本元素和动作。
  • 第4章: “表达式语言”,本章介绍了JSP2.0中最重要的特性“表达式语言”。该特性的目标是帮助开发人员编写无脚本的JSP页面,让JSP页面更加简洁而且有效。本章将帮助你学会通过EL来访问JavaBean和上下文对象。
  • 第5章: “JSTL“,本章介绍了JSP技术中最重要的类库:标准标签库——一组帮助处理常见问题的标签。具体内容包括访问Map或集合对象、条件判断、XML处理,以及数据库访问和数据处理。
  • 第6章: “自定义标签”,大多数时候,JSTL用于访问上下文对象并处理各种任务,但对于特定的任务,我们需要编写自定义标签,本章将介绍如何编写标签。
  • 第7章: “标签文件”,本章介绍在JSP2.0中引入的新特性——标签文件,标签文件可以简化自定义标签的编写。第8章: “监听器”,本章介绍了Servlet中的事件驱动编程,展示了Servlet API中的事件类以及监控器接口,以及如何应用。
  • 第8章: “监听器”,本章介绍了Servlet中的事件驱动编程,展示了Servlet API中的事件类以及监控器接口,以及如何应用。
  • 第9章: “Filters“,本章介绍了FilterAPI,包括FilterFilterConfigFilterChain接口,并展示了如何编写一个Filter实现。
  • 第10章: “修饰RequestsResponses“,本章介绍如何用修饰器模式来包装Servlet请求和响应对象,并改变Servlet请求和响应的行为。
  • 第11章: “异步处理”,本章主要讨论Servlet3.0引入的新特性——异步处理。该特性非常适合于当Servlet应用负载较高且有一个或多个耗时操作。该特性允许由一个新线程来运行耗时操作,使得当前的Web请求处理线程可以处理新的Web请求。
  • 第12章: “安全”,介绍了如何通过声明式以及编程式来保护JavaWeb应用,本章覆盖四个主题:认证、授权、加密和数据完整性。
  • 第13章: “部署”,介绍了Servlet/JSP应用的部署流程,以及部署描述符。
  • 第14章: “动态加载以及Servlet容器加载器”介绍了Servlet3.0中的两个新特性,动态注册支持在无须重启Web应用的情况下注册新的Web对象,以及框架开发人员最关心的容器初始化。

第二部分:Spring MVC

  • 第15章: “Spring框架”,介绍了最流行的开源框架。
  • 第16章: “模型2和MVC模式”,讨论了SpringMVC所实现的设计模式。
  • 第17章: “SpringMVC介绍”,SpringMVC概述。本章编写了第一个SpringMVC应用。
  • 第18章: “基于注解的控制器”,讨论了MVC模式中最重要的一个对象—控制器。本章,我们将学会如何编写基于注解的控制器,这是SpringMVC2.5版本引入的方法。
  • 第19章: “数据绑定和表单标签库”,讨论SpringMVC最强大的一个特性,并利用它来展示表单数据。
  • 第20章: “转换器和格式化”,讨论了数据绑定的辅助对象类型。
  • 第21章: “验证器”,本章将展示如何通过验证器来验证用户输入数据。
  • 第22章: “国际化”,本章将展示如何用SpringMVC来构建多语言网站。
  • 第23章: “上传文件”,介绍两种不同的方式来处理文件上传。
  • 第24章: “下载文件”,介绍如何用编程方式向客户端传输一个资源。

附录

  • 附录A: “Tomcat“,介绍如何安装和配置Tomcat
  • 附录B: “WebAnnotations“,列出所有可用配置Web对象,如ServletListenerFilter的注解。这些来自Servlet3.0规范的注解可以帮助减少部署描述配置。
  • 附录C: “SSL证书”,介绍了如何用KeyTool工具生成公钥/私钥对,并生成数字证书。

下载示例应用

本书所有的示例应用压缩包可以通过如下地址下 载: http://books.brainysoftware.com/download

前言-2-HTTP

HTTP

HTTP协议使得Web服务器与浏览器之间可以通过互联网或内网进行数据交互。万维网联盟(W3C), 作为一个制定标准的国际社区,负责和维护HTTP协议。HTTP第一个版本是HTTP0.9,之后是HTTP 1.0,当前最新版本是HTTP 1.1HTTP 1.1版本的RFC编号是2616,下载地址为http://www.w3.org/Protocols/HTTP/1.1/rfc2616.pdf。按计划,HTTP的下一个版本是HTTP/2

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

注意
2011年,标准化组织IETF发布了WebSocket协议,即RFC 6455规范。该协议允许一个HTTP连接升级为WebSocket连接,支持双向通信,这就使得服务端可以通过WebSocket协议主动发起同客户端的会话通信。

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

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

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

1
protocol://[host.]domain[:port][/context][/resource][?query string]

或者

1
protocol://IP address[:port][/context][/resource][?query string]

中括号中的内容是可选的,因此一个最简的URL http://yahoo.ca 或者http://192.168.1.9
需要说明的是,除了输入http://google.com,你还可以用http://209.85.143.99来访问谷歌。可以用ping命令来获取域名所对应的IP地址:

1
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**, 为了能正确访问,必须提供输入端口号:

1
http://localhost:8080

localhost作为一个保留关键字,用于指向本机
URL中的context部分用来代表应用名称,该部分也是可选的。一台Web服务器可以运行多个上下文(应用),其中一个可以配置为默认上下文,对于访问默认上下文中的资源,可以跳过context部分。

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

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

HTTP请求

一个HTTP请求包含三部分内容:

  • 方法-URI-协议/版本
  • 请求头信息
  • 请求正文如下为一个具体示例:
1
2
3
4
5
6
7
8
9
10
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 (Windows NT 6.3; Win64; x64) AppleWebKi t/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36
Content-Length: 30
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate

lastName=Blanks&firstName=Mike

请求的第一行即是:方法-URI-协议/版本

1
POST /examples/default.jsp HTTP/1.1

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

HTTP 1.1规范定义了7种类型的方法,包括GETPOSTHEADOPTIONSPUTDELETE以及 TRACE,其中GETPOST广泛应用于互联网应用。

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

HTTP请求所包含的请求头信息包含关于客户端环境以及实体内容等非常有用的信息。例如,浏览器所设置的语言实体内容长度等。每个header用回车/换行(即 CRLF)分隔
**HTTP请求头信息和请求正文``用一行空行分隔**, HTTP服务器据此判断请求正文的起始位置。因此在一些关于互联网的书籍中,CRLF作为HTTP`请求的第四种组件。 在此前所举的例子中,请求正文如下行:

1
lastName=Blanks&firstName=Mike

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

HTTP响应

HTTP请求一样,HTTP响应包含三部分:

  • 协议—状态码—描述
  • 响应头信息
  • 响应正文

如下是一个HTTP响应实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Date: Thu, 8 Jan 2015 13:13:33 GMT
Content-Type: text/html
Last-Modified: Wed, 7 Jan 2015 13:13:12 GMT
Content-Length: 112

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

类似于HTTP请求报文,HTTP响应报文第一行:

1
HTTP/1.1 200 OK

说明 了HTTP协议的版本是1.1,并且请求结果是成功的(状态代码200为响应成功)。

HTTP请求报文头信息一样,HTTP响应报文的响应头信息也包含了大量有用的信息。
HTTP响应报文的响应正文HTML文档。**HTTP响应报文的响应头信息和响应正文之间也是用\r\n(CRLF)分隔的**。

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

前言 1Servlet/JSP应用架构

Servlet是一个Java程序,一个Servlet应用有一个或多个Servlet程序。JSP页面会被转换和编译成Servlet程序。
Servlet应用无法独立运行,必须运行在Servlet容器中。Servlet容器将用户的请求传递给Servlet应用,并将Servlet应用生成的结果返回给用户。由于大部分Servlet应用都包含多个 JSP页面,因此更准确地说是“Servlet/JSP应用”。
Web用户通过Web浏览器例如IEMozilla Firefox 或者谷歌Chrome来访问Servlet应用。通常,Web浏览器又叫Web客户端。
Web服务器和Web客户端间通过HTTP协议通信, 因此**Web服务器也叫HTTP服务器**。下面会详细讨论 HTTP协议。

Servlet/JSP容器是一个可以同时处理Servlet和静态内容的Web容器。过去,由于通常认为HTTP服务器比 Servlet/JSP容器更加可靠,因此人们习惯将Servlet/JSP 容器作为HTTP服务器如Apache HTTP服务器的一个模块。这种模式下,HTTP服务器用来处理静态资源,而 Servlet/JSP容器则负责生成动态内容。如今, Servlet/JSP容器更加成熟可靠,并被广泛地独立部署。 Apache TomcatJetty是当前最流行的Servlet/JSP容器, 并且它们是免费而且开源的。你可以访问 http://tomcat.apache.org 以及http://www.eclipse.org/jetty 下载。
ServletJSP只是Java企业版中众多技术中的两个,其他Java EE技术还有Java消息服务,企业Java对象、JavaServer Faces以及Java持久化等,完整的Java EE 技术列表可以访问如下地址:
http://www.oracle.com/technetwork/java/javaee/tech/index.html
要运行Java EE应用,需要一个Java EE容器,例如 GlassFishJBossOracle Weblogic或者IBM WebSphere。诚然,我们可以将一个Servlet/JSP应用部署到一个Java EE容器上,但一个Servlet/JSP容器就已经满足需要了,并且更加轻量。当然,TomcatJetty不是 Java EE容器,因此无法运行EJBJMS技术。

16.1 模型1介绍

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

第16章 模型2和MVC模式

Java Web应用开发中有两种设计模型,为了方便,分别称为模型1和模型2。

  • 模型1以页面为中心,适合于小应用开发。
  • 模型2基于MVC模式,是Java Web应用的推荐架构(简单类型的应用除外)。

本章将会讨论模型2,并展示3个不同示例应用。第一个应用是一个基本的模型2应用,采用Servlet作为控制器,第二个应用引入了控制器,第三个应用引入了验证控件来校验用户的输入。