Java Web应用开发中有两种设计模型,为了方便, 分别称为模型1和模型2。模型1是页面中心,适合于小 应用开发。而模型2基于MVC模式,是Java Web应用的 推荐架构(简单类型的应用除外)。

一. 模型1介绍

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

SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。

二.  模型2介绍

  模型2基于模型-视图-控制器(MVC)模式,该模 式是Smalltalk-80用户交互的核心概念,那时还没有设 计模式的说法,当时称为MVC范式。 一个实现MVC模式的应用包含模型、视图和控制 器3个模块。视图负责应用的展示。模型封装了应用的 数据和业务逻辑。控制器负责接收用户输入、改变模型 以及调整视图的显示。

注:

  Steve Burbeck博士的论文:Applications Programming in Smalltalk80(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 和 MVC 模式 随笔 第1张

  每个HTTP请求都发送给控制器,请求中的URI标 识出对应的action。action代表了应用可以执行的一个操 作。一个提供了Action的Java对象称为action对象。一个 action类可以支持多个actions(在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之Servlet控制器

  示例应用名为app16a,其功能设定为输入一个产品 信息。具体为:用户填写产品表单并提 交;示例应用保存产品并展示一个完成页面,显示已保 存的产品信息。

模型2 和 MVC 模式 随笔 第2张

  示例应用支持如下两个action:

   (1)展示“添加产品”表单。该action发送输入表单到浏览器上,其对应的URI应包含字符串 product_input。

   (2)保存产品并返回图所示的完成页面,对 应的URI必须包含字符串product_save。

模型2 和 MVC 模式 随笔 第3张

  示例应用app16a由如下组件构成:

  (1)一个Product类,作为product的领域对象。

  (2)一个ProductForm类,封装了HTML表单的输 入项。

  (3)一个ControllerServlet类,本示例应用的控制 器。

  (4)一个SaveProductAction类。

  (5)两个JSP页面(ProductForm.jsp和 ProductDetail.jsp)作为view。

  (6)一个CSS文件,定义了两个JSP页面的显示风 格。

pp16a结构如图所示。

模型2 和 MVC 模式 随笔 第4张

 模型2 和 MVC 模式 随笔 第5张

 

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

1. Product类

  Product实例是一个封装了产品信息的JavaBean。 Product类 包含3个属性:productName、 description和price。

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

  Product类实现了java.io.Serializable接口,其实例可 以安全地将数据保存到HttpSession中。根据Serializable 要求,Product实现了一个serialVersionUID属性。

2. ProductForm类

  表单类与HTML表单相映射,是后者在服务端的代 注意: 表。ProductForm类包含了一个产品的字 符串值。ProductForm类看上去与Product类相似,这就 引出一个问题:ProductForm类是否有存在的必要。

  实际上,表单对象会传递ServletRequest给其他组 件,类似Validator(本章后续段落会介绍)。而 ServletRequest是一个Servlet层的对象,不应当暴露给应 用的其他层。

  另一个原因是,当数据校验失败时,表单对象将用 于保存和展示用户在原始表单上的输入。

注意:

  大部分情况下,一个表单类不需要实现Serializable接口,因为表 单对象很少保存在HttpSession中.

4. ControllerServlet类

  ControllerServlet类继承自 javax.servlet.http.HttpServlet类,其doGet和doPost方法最 终调用process方法,该方法是整个servlet控制器的核 心。

  可能有人好奇为何这个Servlet控制器被命名为 ControllerServlet,实际上,这里遵从了一个约定:所有 Servlet的类名称都带有Servlet后缀。

ControllerServlet类

package servlet;

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 domain.Product;
import form.ProductForm;
@WebServlet(name = "ControllerServlet", urlPatterns = {
"/product_input.action", "/product_save.action" })
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);
}

4.视图

  示例应用包含两个JSP页面。第一个页面 ProductForm.jsp对应于product_input操作,第二个页面 ProductDetails.jsp对应于product_save操作。

ProductForm.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body id="global">
    <form action="product_save.action"  method="post" >
     <!--  围绕数据的Fieldset  -->
        <fieldset >
        <legend>Add a Product</legend>
        <p>
        <label for="name'">Product name: </label>
        <input type="text" name="name"  id = "name" tabindex="2" /> <br />
        </p>
        <p>
        <!--  当用户选择该标签时,浏览器就会自动将焦点转到和标签相关的表单控件上。  -->
        <label for="description" >Description:</label>
         <input type="text" name="description"  id = "description"  tabindex="1" /> <br />
         </p>
         <p>
         <label for="price" >Price: </label>
          <input type="text" name="price"  id="price"  tabindex="3" /> <br />
        </p>
        <input type="reset" value="reset"  id ="reset" tabindex="5"/>
        <input type="submit" value="Add product"  id =" submit" tabindex="4"/>
        <p>
        </fieldset>
    </form>
</body>
</html>

ProductDetails.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<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>
            ProductName: ${product.name }<br />
            productScription: ${product.scription} <br />
            productPrice: $${product.price}
        </p>
</div>
</body>
</html>

  ProductForm.jsp页面包含了一个HTML表单。页面 没有采用HTML表格方式进行布局,而采用了位于css 目录下的main.css中的CSS样式表进行控制。

  ProductDetails.jsp页面通过表达式语言(EL)访问 HttpServletRequest所包含的product对象。

  本示例应用作为一个模型2的应用,可以通过如下 几种方式避免用户通过浏览器直接访问JSP页面:

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

5.  测试应用

  假定示例应用运行在本机的8080端口上,则可以通 过如下URL访问应用:

http://localhost:8080/app16a/product_input.action

模型2 和 MVC 模式 随笔 第6张

  完成输入后,表单提交到如下服务端URL上:

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

注意:

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

四. 解耦控制器代码

   

扫码关注我们
微信号:SRE实战
拒绝背锅 运筹帷幄