Spring MVC -- MVC模式

Posted zyly

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring MVC -- 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,使模型、视图与控制器分离,会增加结构的复杂性,并可能产生过多的更新操作,降低运行效率;
  • 视图与控制器间的过于紧密的连接并且降低了视图对模型数据的访问:视图与控制器是相互分离,但却是联系紧密的部件,视图没有控制器的存在,其应用是很有限的,反之亦然,这样就妨碍了他们的独立重用;依据模型操作接口的不同,视图可能需要多次调用才能获得足够的显示数据。对未变化数据的不必要的频繁访问,也将损害操作性能;

 二 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页面的显示风格。

示例应用目录结构如下:

技术图片

项目右键属性、部署路径设置如下:

技术图片

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是一个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 标签来布局表单,使用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服务器,然后启动服务器,假设示例应用运行在本机的8080端口上,则可以通过如下URL访问应用:

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

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

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

参考文章

[1]MVC简介(部分转载)

[2]Spring MVC 学习总结(一)——MVC概要与环境配置(IDea与Eclipse示例)(推荐)

[3]Spring MVC 学习总结(三)——请求处理方法Action详解

[4]Spring MVC学习指南

以上是关于Spring MVC -- MVC模式的主要内容,如果未能解决你的问题,请参考以下文章

java代码spring-mvc模式怎样实现从后台直接定时发送邮件

MVC模式和Spring MVC初识

8spring mvc

Spring Mvc模式下Jquery Ajax 与后台交互操作

狂神说 spring mvc 完整版

Spring 框架基础(06):Mvc架构模式简介,执行流程详解