模型2 和 MVC 模式
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方法来访问参数,同时 支持持久化。
每个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,其功能设定为输入一个产品 信息。具体为:用户填写产品表单并提 交;示例应用保存产品并展示一个完成页面,显示已保 存的产品信息。
示例应用支持如下两个action:
(1)展示“添加产品”表单。该action发送输入表单到浏览器上,其对应的URI应包含字符串 product_input。
(2)保存产品并返回图所示的完成页面,对 应的URI必须包含字符串product_save。
示例应用app16a由如下组件构成:
(1)一个Product类,作为product的领域对象。
(2)一个ProductForm类,封装了HTML表单的输 入项。
(3)一个ControllerServlet类,本示例应用的控制 器。
(4)一个SaveProductAction类。
(5)两个JSP页面(ProductForm.jsp和 ProductDetail.jsp)作为view。
(6)一个CSS文件,定义了两个JSP页面的显示风 格。
pp16a结构如图所示。
所有的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
完成输入后,表单提交到如下服务端URL上:
http://localhost:8080/app16a/product_save.action
注意:
可以将Servlet控制器作为默认主页。这是一个非常重要的特性, 使得在浏览器地址栏中仅输入域名(如http://example.com),就可以 访问到该Servlet控制器,这是无法通过filter方式完成的。
四. 解耦控制器代码
