《Spring MVC學習指南(第2版)》——2.3 模型2之Servlet控製器
本節書摘來自異步社區《Spring MVC學習指南(第2版)》一書中的第2章,第2.3節,作者:【美】Paul Deck著,更多章節內容可以訪問雲棲社區“異步社區”公眾號查看
2.3 模型2之Servlet控製器
為了便於對模型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目錄結構
下麵詳細介紹示例應用的每個組件。
2.3.1 Product類
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屬性。
2.3.2 ProductForm類
表單類與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;
}
}
2.3.3 ControllerServlet類
ControllerServlet類(見清單2.3)繼承自javax.servlet.http.HttpServlet類。其doGet和doPost方法最終調用process方法,該方法是整個Servlet控製器的核心。
可能有人好奇,為何這個Servlet控製器命名為ControllerServlet,實際上,這裏遵從了一個約定:所有Servlet的類名稱都帶有Servlet後綴。
清單2.3 ControllerServlet類
package appdesign1.controuer;
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 ("input-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 Bigoecimal(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);
}
}
}
ontrollerServlet的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 ("input-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);
}
2.3.4 Action類
這個應用中隻有一個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方法是一個空實現。我們會在本章後續章節中實現它。
2.3.5 視圖
示例應用包含兩個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 type="text" name="name"
placeholder="The complete product name">
</label>
<label>
<span>Description: </span>
<input type="text" name="description"
placeholder="Product description">
</label>
<label>
<span>Price: </label>
<input name="price" type="number" step="any"
placeholder="Product price in #.## format">
</label>
<label>
<span> : </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 >
<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頁麵。
將JSP頁麵都放到WEB-INF目錄下。WEB-INF目錄下的任何文件或子目錄都受保護,無法通過瀏覽器直接訪問,但控製器依然可以轉發請求到這些頁麵。
利用一個servlet filter過濾JSP頁麵。
在部署描述符中為JSP頁麵增加安全限製。這種方式相對容易些,無需編寫filter代碼。
2.3.6 測試應用
假定示例應用運行在本機的8080端口上,則可以通過如下URL訪問應用:
https://localhost:8080/appdesign1/input-product
瀏覽器將顯示圖2.2的內容。
完成輸入後,表單提交到如下服務端URL上:
https://localhost:8080/appdesign1/save-product
注意
可以將servlet控製器作為默認主頁。這是一個非常重要的特性,使得在瀏覽器地址欄中僅輸入域名(如https://example.com),就可以訪問到該servlet控製器,這是無法通過filter方式完成的。
最後更新:2017-05-27 12:31:45