閱讀175 返回首頁    go 阿裏雲 go 技術社區[雲棲]


《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)。

screenshot

圖2.2 產品表單

screenshot

圖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所示。

screenshot

圖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>&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 >
  <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

  上一篇:go  《易學C++(第2版)》——1.5 C語言、C++語言和Visual C++
  下一篇:go  《Spring MVC學習指南(第2版)》——2.2 模型2介紹