Spring MVC -- MVC設計模式(演示4個基於MVC框架的案例)


對於簡單的Java Web項目,我們的項目僅僅包含幾個jsp頁面,由於項目比較小,我們通常可以通過鏈接方式進行jsp頁面間的跳轉。

但是如果是一個中型或者大型的項目,上面那種方式就會帶來許多維護困難,代碼復用率低等問題。因此,我們推薦使用MVC模式。

一 MVC概念

1、什么是MVC

MVC的全名是Model View Controller,是模型(model)-視圖(view)-控制器(controller)的縮寫,是一種軟件設計模式。它是用一種業務邏輯、數據與界面顯示分離的方法來組織代碼,將眾多的業務邏輯聚集到一個部件里面,在需要改進和個性化定制界面及用戶交互的同時,不需要重新編寫業務邏輯,達到減少編碼的時間。

MVC開始是存在於桌面程序中的,M是指業務模型,V是指用戶界面,C則是控制器。

使用的MVC的目的:在於將M和V的實現代碼分離,從而使同一個程序可以使用不同的表現形式。比如Windows系統資源管理器文件夾內容的顯示方式,下面兩張圖中左邊為詳細信息顯示方式,右邊為中等圖標顯示方式,文件的內容並沒有改變,改變的是顯示的方式。不管用戶使用何種類型的顯示方式,文件的內容並沒有改變,達到M和V分離的目的。

在網頁當中:

  • V:即View視圖是指用戶看到並與之交互的界面。比如由html元素組成的網頁界面,或者軟件的客戶端界面。MVC的好處之一在於它能為應用程序處理很多不同的視圖。在視圖中其實沒有真正的處理發生,它只是作為一種輸出數據並允許用戶操縱的方式;
  • M:即model模型是指模型表示業務規則。在MVC的三個部件中,模型擁有最多的處理任務。被模型返回的數據是中立的,模型與數據格式無關,這樣一個模型能為多個視圖提供數據,由於應用於模型的代碼只需寫一次就可以被多個視圖重用,所以減少了代碼的重復性;
  • C:即controller控制器是指控制器接受用戶的輸入並調用模型和視圖去完成用戶的需求,控制器本身不輸出任何東西和做任何處理。它只是接收請求並決定調用哪個模型構件去處理請求,然后再確定用哪個視圖來顯示返回的數據;

下圖說明了三者之間的調用關系:

用戶首先在界面中進行人機交互,然后請求發送到控制器,控制器根據請求類型和請求的指令發送到相應的模型,模型可以與數據庫進行交互,進行增刪改查操作,完成之后,根據業務的邏輯選擇相應的視圖進行顯示,此時用戶獲得此次交互的反饋信息,用戶可以進行下一步交互,如此循環。

常見的服務器端MVC框架有:Struts、Spring MVC、ASP.NET MVC、Zend Framework、JSF;常見前端MVC框架:vue、angularjs、react、backbone;由MVC演化出了另外一些模式如:MVP、MVVM。

注意:我們應該避免用戶通過瀏覽器直接訪問jsp頁面。

2、MVC舉例一(jsp+servlet+javabean)

最典型的MVC就是jsp+servlet+javabean模式:

  • Serlvet作為控制器,用來接收用戶提交的請求,然后獲取請求中的數據,將之轉換為業務模型需要的數據模型,然后調用業務模型相應的業務方法進行更新(這一塊也就是Model層所做的事情),同時根據業務執行結果來選擇要返回的視圖;
  • JavaBean作為模型,既可以作為數據模型來封裝業務數據(對應實體類),又可以作為業務邏輯模型來包含應用的業務操作(對應Action類)。其中,數據模型用來存儲或傳遞業務數據,而業務邏輯模型接收到控制器傳過來的模型更新請求后,執行特定的業務邏輯處理,然后返回相應的執行結果;實踐中會采用一個實體類來持有模型狀態,並將業務邏輯放到一個Action類中。
  • JSP作為表現層,負責提供頁面為用戶展示數據,提供相應的表單(Form)來用於用戶的請求,並在適當的時候(點擊按鈕)向控制器發出請求來請求模型進行更新;

每個控制器中可以定義多個請求URL,每個用戶請求都發送給控制器,請求中的URL標識出對應的Action。Action代表了Web應用可以執行的一個操作。一個提供了Action的Java對象稱為Action對象。一個Action類型可以支持多個Action(在Spring MVC以及Struts2中),或一個Action(在struts1中)。

注意:Struts1、Spring MVC和JavaServer Fces使用Servlet作為控制器,而Struts2使用Filter作為控制器。

3、MVC舉例二(Struts2框架)

Struts2框架:Struts2是基於MVC的輕量級的web應用框架。Struts2的應用范圍是Web應用,注重將Web應用領域的日常工作和常見問題抽象化,提供一個平台幫助快速的完成Web應用開發。基於Struts2開發的Web應用自然就能實現MVC,Struts2着力於在MVC的各個部分為開發提供相應幫助。

下面通過代碼來簡單解釋一下(這里只是簡單使用):

Login.html(位於WebContent下)

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
    <form id="form1" name="form1" action="/action/Login.action" method="post">
        登錄<br>
        用戶名:<input name="username" type="text"><br>
        密碼:<input name="password" type="password"><br>
        <input type="submit" value="登錄">
    </form>
</body>
</html>

LoginAction.Java(位於包com.dc365.s2下)

    if(username.equals("1") && password.equals("1")) {
            return "Success";
    }
    return "Error";

struts.xml(位於src下)

<struts>
    <package name="default" namespcase="/action" extends="struts-default">
        <action name="Login" class="com.dc365.s2.LoginAction">
            <result name="Success">Success.jsp</result>
            <result name="Error">Error.jsp</result>
        </action>
    </package>
</struts>

注意:除了上面代碼,還需要在web.xml里面配置前端過濾器FilterDispatcher。

用戶首先在Login.html中輸入用戶名和密碼,點擊登陸,用戶請求(請求路徑為/action/Login.action)首先到達前端控制器FilterDispatcher,FilterDispatcher根據用戶請求的URL和配置在struts.xml找到對應的Login,然后根據對應的class的路徑進入相應的login.Java,在這里判斷之后,返回success或error,然后根據struts.xml中的result值,指向相應的jsp頁面。

  • 控制器——filterdispatcher:從上面這張圖來看,用戶請求首先到達前端控制器FilterDispatcher。FilterDispatcher負責根據用戶提交的URL和struts.xml中的配置,來選擇合適的動作(Action),讓這個Action來處理用戶的請求。FilterDispatcher其實是一個過濾器(Filter,servlet規范中的一種web組件),它是Struts2核心包里已經做好的類,不需要我們去開發,只是要在項目的web.xml中配置一下即可。FilterDispatcher體現了J2EE核心設計模式中的前端控制器模式。
  • 動作——Action:在用戶請求經過FilterDispatcher之后,被分發到了合適的動作Action對象。Action負責把用戶請求中的參數組裝成合適的數據模型,並調用相應的業務邏輯進行真正的功能處理,獲取下一個視圖展示所需要的數據。Struts2的Action,相比於別的web框架的動作處理,它實現了與Servlet API的解耦,使得Action里面不需要再直接去引用和使用HttpServletRequest與HttpServletResponse等接口。因而使得Action的單元測試更加簡單,而且強大的類型轉換也使得我們少做了很多重復的工作。
  • 視圖——Result:視圖結果用來把動作中獲取到的數據展現給用戶。在Struts2中有多種優秀的結果展示方式,常規的jsp,模板freemarker、velocity,還有各種其它專業的展示方式,如圖表jfreechart、報表JasperReports、將XML轉化為HTML的XSLT等等。而且各種視圖結果在同一個工程里面可以混合出現。

4、MVC優點

  • 耦合性低:視圖層和業務層分離,這樣就允許更改視圖層代碼而不用重新編譯模型和控制器代碼,同樣,一個應用的業務流程或者業務規則的改變只需要改動MVC的模型層即可。因為模型與控制器和視圖相分離,所以很容易改變應用程序的數據層和業務規則;
  • 重用性高:MVC模式允許使用各種不同樣式的視圖來訪問同一個服務器端的代碼,因為多個視圖能共享一個模型,它包括任何WEB(HTTP)瀏覽器或者無線瀏覽器(wap),比如,用戶可以通過電腦也可通過手機來訂購某樣產品,雖然訂購的方式不一樣,但處理訂購產品的方式是一樣的。由於模型返回的數據沒有進行格式化,所以同樣的構件能被不同的界面使用;
  • 部署快,生命周期成本低:MVC使開發和維護用戶接口的技術含量降低。使用MVC模式使開發時間得到相當大的縮減,它使程序員(Java開發人員)集中精力於業務邏輯,界面程序員(HTML和JSP開發人員)集中精力於表現形式上;
  • 可維護性高:分離視圖層和業務邏輯層也使得WEB應用更易於維護和修改;

5、MVC缺點

  • 完全理解MVC比較復雜:由於MVC模式提出的時間不長,加上同學們的實踐經驗不足,所以完全理解並掌握MVC不是一個很容易的過程;
  • 調試困難:因為模型和視圖要嚴格的分離,這樣也給調試應用程序帶來了一定的困難,每個構件在使用之前都需要經過徹底的測試;
  • 不適合小型,中等規模的應用程序:在一個中小型的應用程序中,強制性的使用MVC進行開發,往往會花費大量時間,並且不能體現MVC的優勢,同時會使開發變得繁瑣;
  • 增加系統結構和實現的復雜性:對於簡單的界面,嚴格遵循MVC,使模型、視圖與控制器分離,會增加結構的復雜性,並可能產生過多的更新操作,降低運行效率;
  • 視圖與控制器間的過於緊密的連接並且降低了視圖對模型數據的訪問:視圖與控制器是相互分離,但卻是聯系緊密的部件,視圖沒有控制器的存在,其應用是很有限的,反之亦然,這樣就妨礙了他們的獨立重用;依據模型操作接口的不同,視圖可能需要多次調用才能獲得足夠的顯示數據。對未變化數據的不必要的頻繁訪問,也將損害操作性能;

6、具體案例

接下來我們將會演示基於MVC框架的4個不同的示例:

  • 第一個采用Servlet作為控制器;
  • 第二個采用Filter作為控制器;
  • 第三個引入校驗器組件來校驗用戶輸入數據的合法性;
  • 第四個采用了一個自制的依賴注入器。在實際的應用中,我們應該使用Spring。

二 MVC案例(Serlvet作為控制器)

創建一個名為appdesign1的Dynamic Web Project項目,Servlet版本選擇3.0,其功能設定為輸入一個產品信息。具體為:

  • 用戶填寫產品表單並提交;
  • 保存產品並展示一個完成頁面,顯示已經保存的產品信息;

示例應用支持如下兩個action(每個action對應一個URL):

  • 展示”添加產品“表單,其對應的URL包含字符串input-product;
  • 保存產品並返回完成界面,對應的URL包含字符串save-product;

示例應用由如下組件組成:

  •  一個Product類,作為product的領域對象;
  • 一個ProductForm類,封裝了HTML表單的輸入項;
  • 一個ControllerServlet,本示例應用的控制器;
  • 一個SaveProdtcuAction類;
  • 兩個jsp頁面(ProductForm.jsp和ProductDetails.jsp)作為視圖;
  • 一個CSS文件,定義了兩個jsp頁面的顯示風格。

示例應用目錄結構如下:

注意:由於我們采用的是Servlet3.0版本,web.xml可以不需要,具體可以參考博客Servlet2.5版本和Servlet3.0版本

項目右鍵屬性、部署路徑設置如下:

1、Product類

Product類是一個封裝了產品信息的JavaBean。Product類包含三個屬性:name,description和price:

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、ProductForm表單類

表單類與HTML表單相對應,是后者在服務器的代表。ProductForm類看上去同Product類相似,這就引出一個問題: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;
    }
}

實際上,通過表單對象可以將ServletRequest中的表單信息傳遞給其它組件,比如校驗器Validator(后面會介紹,主要用於檢查表單輸入數據是否合法)。如果不使用表單對象,則應將ServletRequest傳遞給其它組件,然而ServletRequest是一個Servlet層的對象,是不應當暴露給應用的其它層。

另一個原因是,當數據校驗失敗時,表單對象將用於保存和顯示用戶在原始表單上的輸入。

注意:大部分情況下,一個表單類不需要事先Serializable接口,因為表單對象很少保存在HttpSession中。

3、ControllerServlet類

ContrlooerServlet類繼承自javax.servlet.http.HttpServlet類。其doGet()和doPost()方法最終調用process()方法,該方法是整個Servlet控制器的核心。

可能有人好奇,為何Servlet控制器命名為ControllerServlet,實際上,這里遵從了一個約定:所有Servlet的類名稱都帶有Servlet后綴。

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.ProductForm;
import appdesign1.model.Product;
import java.math.BigDecimal;
//Servlet3.0使用注解指定訪問Servlet的URL
@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  根據不同的uri執行不同的action
        String dispatchUrl = null;
        if ("input-product".equals(action)) {
            // no action class, just forward
            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 創建一個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或sava-product。

接着,當action值為sava-product,process()方法執行如下步驟:

  • 創建並更根據請求參數創建一個ProductForm表單對象。save-product操作涉及3個屬性:name,description、price。然后創建一個product對象,並通過表單對象設置相應屬性;
  • 執行針對product對象的業務邏輯,保存表單;
  • 轉發請求到視圖(jsp頁面),顯示輸入的表單信息。

process()方法中判斷action的if代碼塊如下:

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

對於input-product,無需任何操作;而針對save-product,則創建一個ProductForm對象和Product對象,並將前者的屬性值復制到后者。

再次,process()方法實例化SavaProductAction類,並調用其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 創建一個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 model in a scope variable for the view    
            request.setAttribute("product", product);
            dispatchUrl = "/jsp/ProductDetails.jsp";

最后,process()方法轉到視圖,如果action是input-product,則轉到ProductForm.jsp頁面,否則轉到ProductDetails.jsp頁面:

        //請求轉發
        if (dispatchUrl != null) {
            RequestDispatcher rd =
                    request.getRequestDispatcher(dispatchUrl);
            rd.forward(request, response);
        }

4、Action類

這個應用這有一個action類,負責將一個product對象持久化,例如數據庫。這個action類名為SaveProductAction:

package appdesign1.action;

import appdesign1.model.Product;

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

在這個示例中,SaveProductAction類的save()方法是一個空實現。

5、視圖

示例應用包含兩個jsp頁面。第一個頁面ProductForm.jsp對應input-product操作,第二個頁面ProductDetails.jsp對應sava-product操作。

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 :</span>
        <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 table標簽來布局表單,使用CSS。

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頁面包含了一個HTML表單。ProductDetails頁面通過EL表達式語言訪問HttpServletRequest所包含的product對象。

此外,該實例存在一個問題,即用戶可以直接通過瀏覽器訪問這兩個jsp頁面,我們可以通過以下方式避免這種直接訪問:

  • 將jsp頁面都放在WEB-INF目錄下。WEB-INF目錄下的任何文件和子目錄都受保護、無法通過瀏覽器直接訪問,但控制器仍然可以轉發請求到這些頁面;
  • 利用一個servlet filter過濾jsp頁面;
  • 在部署描述符中為jsp壓面增加安全限制。這種方式相對簡單,無需編寫filter代碼;

main.css:

form {
    margin-left:auto;
    margin-right:auto;
    max-width: 450px;
    background: palegreen;
    padding: 25px 15px 25px 10px;
    border:1px solid #dedede;
    font: 12px Arial;
}
h1 {
    padding: 20px;
    display: block;
    border-bottom:1px solid grey;
    margin: -20px 0px 20px 0px;
    color: mediumpurple;
}
h1>span {
    display: block;
    font-size: 13px;
}
label {
    display: block;
}
label>span {
    float: left;
    width: 20%;
    text-align: right;
    margin: 14px;
    color: mediumpurple;
    font-weight:bold;
}
input[type="text"], input[type="number"] {
    border: 1px solid #dedede;
    height: 30px;
    width: 70%;
    font-size: 12px;
    border-radius: 3px;
    margin: 5px;
}
input[type="submit"] {
    background: mediumseagreen;
    font-weight: bold;
    border: none;
    padding: 8px 20px 8px 20px;
    color: black;
    border-radius: 5px;
    cursor: pointer;
    margin-left:4px;
}
input[type="submit"]:hover {
    background: red;
    color: yellow;
}

6、測試應用

將項目部署到tomcat服務器,然后啟動服務器,假設示例應用運行在本機的8000端口上,則可以通過如下URL訪問應用:

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

完成表單輸入后,表單提交到如下服務器URL上:

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

三 MVC案例(Filter作為控制器)

雖然Servlet是MVC框架中最常見的控制器,但是過濾器也可以充當控制器。Struts2就是使用過濾器作為控制器,是因為該過濾波器也可用於提供靜態頁面。

Filter是在Servlet 2.3之后增加的新功能,當需要限制用戶訪問某些資源或者在處理請求時提前處理某些資源的時候,就可以使用過濾器完成。
過濾器是以一種組件的形式綁定到WEB應用程序當中的,與其他的WEB應用程序組件不同的是,過濾器是采用了“鏈”的方式進行處理的。
在Servlet中,如果要定義一個過濾器,則直接讓一個類實現javax.servlet.Filter接口即可,此接口定義了三個操作方法:
  • public void init(FilterConfig filterConfig) throws ServletException
  • public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain) throws IOException,ServletException
  • public void destroy();
FilterChain接口的主要作用是將用戶的請求向下傳遞給其他的過濾器或者是Servlet:
  • public void doFilter(ServletRequest request,ServletResponse response) throws IOException,ServletException
在FilterChain接口中依然定義了一個同樣的doFilter()方法,這是因為在一個過濾器后面可能存在着另外一個過濾器,也可能是請求的最終目標(Servlet),這樣就通過FilterChain形成了一個“過濾鏈”的操作,所謂的過濾鏈就類似於生活中玩的擊鼓傳花游戲 。

注意:過濾器沒有作為歡迎頁面(即僅僅在瀏覽器地址欄中輸入域名)的權限,僅輸入域名時不會調用過濾器分派器。

下面我們采用一個名為FilterDispactcher的過濾器替代appdesign1項目中的Servlet控制器,項目命名為appdesign2,目錄結構如下:

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;

//Servlet3.0新增了注解的特性,指定過濾器的訪問URL
@WebFilter(filterName = "DispatcherFilter",
        urlPatterns = { "/*" })
public class DispatcherFilter implements Filter {

    //過濾器初始化
    @Override
    public void init(FilterConfig filterConfig)
            throws ServletException {
    }

    //過濾器銷毀
    @Override
    public void destroy() {
         System.out.println("** 過濾器銷毀。");
    }

    //執行過濾操作
    @Override
    public void doFilter(ServletRequest request,
            ServletResponse response, FilterChain filterChain)
            throws IOException, ServletException {
        System.out.println("** 執行doFilter()方法之前。");
        
        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);
        }
        System.out.println("** 執行doFilter()方法之后。");
    }
}

doFilter()方法的內容同appdesign1中的process()方法。

由於過濾器的過濾目標是包括靜態內容在內的所有網址,因此,若沒有相應的action,則需要調用filterChain.doFilter();

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

 要測試應用,將項目配置到tomcat服務器,啟動服務器,並在瀏覽器輸入如下URL:

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

四 表單輸入校驗器

在Web應用執行action時,很重要的一個步驟就是進行輸入校驗。檢驗的內容可以是簡單的,如檢查一個輸入是否為空,也可以是復雜的,如檢驗信用卡號。實際上,因為校驗工作如此重要,Java社區專門發布了JSR 303 Bean Validation以及JSR 349 Bean Validation 1.1版本,將Java世界的輸入校驗進行標准化。現代的MVC框架通常同時支持編程式和聲明式兩種校驗方式。在編程式中,需要通過編碼進行用戶輸入校驗;而在聲明式中,則需要提供包含校驗規則的XML文檔或者屬性文件。

注意:即使您可以使用HTML5或JavaScript執行客戶端輸入校驗,也不要依賴它,因為精明的用戶可以輕松地繞過它。始終執行服務器端輸入驗證!

本節的新應用(appdesign3)擴展自appdesign1,但是多了一個ProductValidator類:

 

1、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()方法返回一個包含錯誤信息的字符串列表,若返回一個空列表,則表示輸入合法。

現在需要讓控制器使用這個校驗器了,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;

//Servlet3.0使用注解指定訪問Servlet的URL
@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);
        
        // execute an action  根據不同的uri執行不同的action
        String dispatchUrl = null;
        if ("input-product".equals(action)) {
            // no action class, there is nothing to be done
            dispatchUrl = "/jsp/ProductForm.jsp";
        } else if ("save-product".equals(action)) {
            // instantiate 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  創建一個Model類
                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);
                dispatchUrl = "/jsp/ProductDetails.jsp";
            } else {         //表單輸入有誤,重新加載該頁面
                request.setAttribute("errors", errors); request.setAttribute("form", productForm); dispatchUrl = "/jsp/ProductForm.jsp"; }
        }

        // forward to a view   請求轉發
        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對象存儲在ServletRequest中,並轉發到ProductDetails.jsp頁面,顯示產品的詳細信息。如果校驗失敗,控制器將錯誤列表和ProductForm對象存儲在ServletRequest中,並返回到ProductForm.jsp中。

 if (errors.isEmpty()) {   //表單輸入正確
                // create Product from ProductForm  創建一個Model類
                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);
                dispatchUrl = "/jsp/ProductDetails.jsp";
            } else {         //表單輸入有誤,重新加載該頁面
                request.setAttribute("errors", errors);
                request.setAttribute("form", productForm);
                dispatchUrl = "/jsp/ProductForm.jsp";
            }
        }

2、ProductForm.jsp

現在,需要修改appdesign3應用的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 id='errors'>" += "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>

3、測試應用

將項目配置到tomcat服務器,啟動服務器,並在瀏覽器輸入如下URL:

http://localhost:8008/appdesign3/input-product

若產品表單提交了無效數據,頁面將顯示錯誤信息,如下:

五 自制的依賴注入

什么是依賴注入技術?如果不了解的話,可以參考博客:Spring MVC -- Spring框架入門(IoC和DI)

示例appdesign4使用了一個自制的依賴注入器。在實際的應用中,我們應該使用Spring。該示例用來生成pdf。它有兩個action:

  • form:沒有action類,只是將請求轉發到用來輸入一些文本的表單;
  • pdf:生成pdf文件並使用PDFAction類,PDFAction類依賴於PDFService類;

該應用的目錄結構如下:

1、PDFAction類和PDFService

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);
    }
}

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類。

package util;

import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.edit.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType1Font;

public class PDFUtil {
    public static void createDocument(String path, String input) {
        PDDocument doc = null;
        try {
            doc = new PDDocument();
            PDFont font = PDType1Font.HELVETICA;
            PDPage page = new PDPage();
            doc.addPage(page);
            float fontSize = 12.0f;

            PDRectangle pageSize = page.getMediaBox();
            float centeredXPosition = (pageSize.getWidth() - fontSize
                    / 1000f) / 2f;
            float stringWidth = font.getStringWidth(input);
 
            PDPageContentStream contentStream = new PDPageContentStream(doc, page);
            contentStream.setFont(font, fontSize);
            contentStream.beginText();
            contentStream.moveTextPositionByAmount(centeredXPosition, 600);
            contentStream.drawString(input);
            contentStream.endText();
            contentStream.close();
            doc.save(path);
            doc.close();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}
View Code

這里的關鍵在於,PDFAction需要一個PDFService來完成它的工作,換句話說,PDFAction依賴於PDFService。沒有依賴注入,你必須在PDFAction類中實例化PDFService類,這將使PDFAction更不可測試。除此之外,如果需要更改PDFService的實現,必須重新編譯PDFAction。

使用依賴注入,每個組件都有注入它的依賴項,這使得測試每個組件更容易。對於在依賴注入環境中的類,必須使其支持注入。一種方法是為每個依賴關系創建一個set方法。例如,PDFAction類有一個setPDFService方法,可以調用它來傳遞PDFService。注入也可以通過構造方法或者類屬性進行。

一旦所有的類都支持注入,則可以選擇一個依賴注入框架並將其導入項目。比如Spring框架、Google Guice、Weld和PicoContainer是一些好的選擇。

2、DependencyInjector類

appdesign4應用使用DependencyInjector類來代替依賴注入框架(在實際應用中,我們應該使用一個合適的框架)。這個類專為appdesign4應用設計,利用Java的反射機制來實現(不懂的可以參考博客:Java基礎 -- 深入理解Java類型信息(Class對象)與反射機制),可以容易的實例化。一旦實例化,必須調用其start()方法來執行初始哈,使用后,應調其shutdown()方法來釋放資源。在此示例中,start()和shutdown()都沒有實現。

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。

3、ControllerServlet

appdesign4應用的Servlet控制器如下所示。請注意:在其init()方法中實例化DependencyInjector,並在其destroy()方法中調用DependencyInjector的shutdown()方法。Servlet不再創建它自身的依賴項,相反,它從DependencyInjector獲取這些依賴。

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;

//Servlet3.0使用注解指定訪問Servlet的URL
@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
        dependencyInjector = new DependencyInjector();
        dependencyInjector.start();
    }
 
    @Override
    public void destroy() {
        //關閉DependencyInjector實例
        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.: /product_input
         */
        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)) {
            //創建pdf文檔
            HttpSession session = request.getSession(true);
            String sessionId = session.getId();
            //利用dependencyInjector創建PDFAction對象
            PDFAction pdfAction = (PDFAction) dependencyInjector
                    .getObject(PDFAction.class);
            String text = request.getParameter("text");
            //設置pdf在磁盤上文件路徑E:\tomcat\wtpwebapps\appdesign4\result\sessionId.pdf
            String path = request.getServletContext()
                    .getRealPath("/result/") + sessionId + ".pdf";
            //System.out.println(path);
            //生成pdf文件,保存在path路徑下
            pdfAction.createPDF(path, text);
            
            // redirect to the new pdf
            StringBuilder redirect = new 
                    StringBuilder();
            redirect.append(request.getScheme() + "://");       // http://
            redirect.append("localhost");                       // http://localhost
            int port = request.getLocalPort();
            if (port != 80) {
                redirect.append(":" + port);                   // http://localhost:8008
            }
            String contextPath = request.getContextPath();     // /appdesign4
            if (!"/".equals(contextPath)) {
                redirect.append(contextPath);                  // http://localhost:8008/appdesign4
            }
            redirect.append("/result/" + sessionId + ".pdf"); 
            //System.out.println(redirect.toString());  
            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:對於表單模式,Servlet控制器將請求轉發到表單/jsp/Form.jsp;
  • pdf:對於pdf模式,Servlet控制器使用DependencyInjector創建PDFAction對象,並調用其createDocument()方法生成pdf文檔,保存在當前項目的result文件夾下。此方法有兩個參數:文件保存在磁盤上的路徑和文本輸入。所有PDF存儲在應用項目目錄下的result文件夾中,用戶的會話標識符用做文件名,而文本輸入作為pdf文件的內容;最后,重定向到生成的pdf文件,以下是創建重定向URL並將瀏覽器重定向到新URL的代碼:
     // redirect to the new pdf
                StringBuilder redirect = new 
                        StringBuilder();
                redirect.append(request.getScheme() + "://");       // http://
                redirect.append("localhost");                       // http://localhost
                int port = request.getLocalPort();
                if (port != 80) {
                    redirect.append(":" + port);                   // http://localhost:8008
                }
                String contextPath = request.getContextPath();     // /appdesign4
                if (!"/".equals(contextPath)) {
                    redirect.append(contextPath);                  // http://localhost:8008/appdesign4
                }
                redirect.append("/result/" + sessionId + ".pdf"); 
                //System.out.println(redirect.toString());  
                response.sendRedirect(redirect.toString());

4、PDFActionTest和PdfBoxTest

該應用提供了兩個測試類PDFActionTest和PdfBoxTest,由於依賴注入器,appdesign4中的每個組件都可以獨立測試,比如可以運行PDFActionTest類來測試類的createDocument()方法。

PDFActionTest類:

package test;

import action.PDFAction;
import util.DependencyInjector;

public class PDFActionTest {
    public static void main(String[] args) {
        //創建DependencyInjector對象
        DependencyInjector dependencyInjector = new DependencyInjector();
        dependencyInjector.start();
        //利用DependencyInjector創建PDFAction對象
        PDFAction pdfAction = (PDFAction) dependencyInjector.getObject(
                PDFAction.class);
        //生成pdf文檔
        pdfAction.createPDF("E:/tomcat/wtpwebapps/appdesign4/result/1.pdf", 
                "Testing PDFAction....");
        dependencyInjector.shutDown();
    }
}

 輸出如下:

PdfBoxTest類:

package test;
import util.PDFUtil;

public class PdfBoxTest {
    public static void main(String[] args) {
        PDFUtil.createDocument("E:/tomcat/wtpwebapps/appdesign4/result/2.pdf", 
                "Tod late");
    }
}

輸出如下:

5、視圖

Form.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="pdf">
    <h1>Create PDF
        <span>Please use this form to enter the text</span>
    </h1>
    <label>
        <span>Text :</span>
        <input type="text" name="text" 
            placeholder="Text for PDF"/>
    </label>
    <label>
        <span>&nbsp;</span> 
        <input type="submit"/> 
    </label> 
</form>
</body>
</html>

main.css:

form {
    margin-left:auto;
    margin-right:auto;
    max-width: 450px;
    background: palegreen;
    padding: 25px 15px 25px 10px;
    border:1px solid #dedede;
    font: 12px Arial;
}
h1 {
    padding: 20px;
    display: block;
    border-bottom:1px solid grey;
    margin: -20px 0px 20px 0px;
    color: mediumpurple;
}
h1>span {
    display: block;
    font-size: 13px;
}
label {
    display: block;
}
label>span {
    float: left;
    width: 20%;
    text-align: right;
    margin: 14px;
    color: mediumpurple;
    font-weight:bold;
}
input[type="text"], input[type="number"] {
    border: 1px solid #dedede;
    height: 30px;
    width: 70%;
    font-size: 12px;
    border-radius: 3px;
    margin: 5px;
}
input[type="submit"] {
    background: mediumseagreen;
    font-weight: bold;
    border: none;
    padding: 8px 20px 8px 20px;
    color: black;
    border-radius: 5px;
    cursor: pointer;
    margin-left:4px;
}
input[type="submit"]:hover {
    background: red;
    color: yellow;
}
View Code

6、應用測試

將項目配置到tomcat服務器,啟動服務器,並在瀏覽器輸入如下URL來測試應用:

http://localhost:8008/appdesign4/form

應用將展示一個表單:

如果在文本字段中輸入一些內容並按提交按鈕,服務器將創建一個pdf文件並發送重定向到瀏覽器:

請注意:重定向網址將采用此格式:

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

參考文章

[1]MVC簡介(部分轉載)

[2]Spring MVC 學習總結(一)——MVC概要與環境配置(IDea與Eclipse示例)(推薦)

[3]Spring MVC 學習總結(三)——請求處理方法Action詳解

[4]Spring MVC學習指南

[5]java基礎篇---Servlet過濾器


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM