MVC设计模式介绍 & SpringMVC实现

2,030 阅读9分钟

什么是MVC

Model 1时代

在最初的 JSP网页中,像数据库查询语句这样的数据层代码和像HTML这样的表示层代码是混在一起,更甚者还会在里面写一些Java代码来做业务逻辑的处理。(在 jsp中 <% 一写就可以在里面敲Java代码)虽然经验比较丰富的开发者会将数据从表示层分离开来,但这样的良好设计通常并不是很容易做到的。

这种老的架构模式,前后端互相依赖,耦合性过高。而且写这种代码还需要你对前端有一定的掌握程度,可以说是十分的痛苦。

但这种模式也要一些自己的好处

  • 架构简单,适合小型项目的开发
    • 但 jsp要做的事情太多了不是很便于维护
    • 加上现在的开发中 jsp用的也越来越少了,这种模式也就慢慢淡出了我们的视线

Model 2时代

正是因为 model 1的种种弊端,Servlet + JSP + Java Bean 的开发模式就诞生了,也就是我们的Model 2

使用了这种架构模式后,我们会将程序分为三个部分 Model、View、Controller

模型(Model):

  • 数据模型层,这一层即负责对数据的封装,也负责对数据进行一些逻辑操作。
  • Model 有对数据直接访问的权力,例如对数据库的访问。
  • Model不依赖 ViewController,也就是说, Model 不关心它会被如何显示或是如何被操作。
  • Model 中数据的变化一般会通过一种刷新机制来实现。
  • 为了实现这种机制,那些用于监视此 ModelView 必须事先在此 Model 上注册,从而,View 可以了解在数据 Model 上发生的改变。

视图(View):

  • 负责数据模型的展示,一般就是我们看到的用户界面。
  • View 中一般没有程序上的逻辑
  • 为了实现 View 上的刷新功能,View 需要访问它监视的数据模型(Model),因此应该事先在被它监视的数据中注册。

控制器(Controller):

  • 起到不同层面间的组织作用,用于控制应用程序的流程。它处理事件并作出响应。“事件”包括用户的行为和数据 Model 上的改变。

这样做有哪些好处呢

尽管构造MVC应用程序需要一些额外的工作,但是它带给我们的好处是毋庸置疑的

  • 首先,多个 View 能共享一个 Model 。如今,同一个Web应用程序会提供多种用户界面,例如用户希望既能够通过浏览器来收发电子邮件,还希望通过手机来访问电子邮箱。这就要求Web网站同时能提供Internet界面和WAP界面。在MVC设计模式中, Model 响应用户请求并返回响应数据,View 负责格式化数据并把它们呈现给用户,业务逻辑和表示层分离,同一个 Model 可以被不同的 View 重用,所以大大提高了代码的可重用性。
  • MVC三个模块相互独立,改变其中一个不会影响其他两个,所以依据这种架构思想能构造良好的低耦合构件。
  • Controller 提高了应用程序的灵活性和可配置性。Controller 可以用来连接不同的 Model 和 View 去完成用户的需求,也可以为构造应用程序提供强有力的手段。
    • 给定一些可重用的 Model 、 View 和Controller 可以根据用户的需求选择适当的 Model 进行处理,然后选择适当的的 View 将处理结果显示给用户。
  • MVC 架构强调职责分离,所以在发展 MVC 应用时会产生很多文件,在 IDE (集成开发环境) 不够成熟时它会是个问题,但在现代主流 IDE 都能使用类别对象的信息来组织代码编辑的情况下,多文件早已不是问题。
  • MVC 架构会要求开发者进一步思考应用程序的架构 (Application Architecture),而非用大杂烩的方式开发应用程序,对于应用程序的生命周期以及后续的可扩展与可维护性而言有相当正面的帮助。
  • MVC 职责分离也带来了一个现代软件工程要求的重要特性:可测试性,MVC-based 的应用程序在良好的职责分离的设计下,各个部分可独立行使单元测试,有利于与企业内的自动化测试、持续集成与持续发行流程集成,减少应用程序改版部署所需的时间。

以下引自维基百科

MVC架构应用程序的目的就是希望打破以往应用程序使用的大杂烩编写方式,并间接诱使开发人员以更高的架构导向思维来思考应用程序的设计,因此对于一个刚入门的初学者来说,架构导向的思考会有一定的门槛,需要较多的实现与练习才能具备相应的能力,大多数的初学者还是较习惯于大杂烩式的程序撰写,所以可能会对 MVC 模式抱持着排斥或厌恶的心态。

然而 MVC 是有助于应用程序长远的发展,虽然大杂烩式的程序也可以用来发展长生命周期的应用程序,但是相较于 MVC,大杂烩式的程序在可扩展性和可维护性 (尤其是可测试性) 上会远比 MVC 复杂很多,相反的,MVC 模式的应用程序是在初始开发时期必须先思考并使用软件架构,使得开发时期会需要花较多心力,但是一旦应用程序完成后,可扩展性、可维护性和可测试性反而会因为 MVC 的特性而变得容易。

MVC 模式在已有众多优秀框架的现代,早就已经没有不适合小型应用的问题,小型的应用还是可以由 MVC Framework 的应用来获取 MVC 的优点,同时它也能作为未来小型应用扩展到大型应用时的基础与入门砖。若一开始就想要做大型应用,那么 MVC 模式的职责分离以及要求开发的架构思考会更适合大型应用的开发。

SpringMVC

SpringMVC 是 Spring框架的一部分,它是基于Java实现的MVC轻量级 web框架

SpringMVC官方文档

springMVC 围绕 DispatcherServlet设计,它是一个请求分发器,所有的请求都会经过它。它来为这些请求寻找对应的处理器(Controller)。

DispatcherServlet是一个实际的Servlet,它继承自HttpServlet的基类。

以下引自官方文档

Spring MVC, as many other web frameworks, is designed around the front controller pattern where a central Servlet, the DispatcherServlet, provides a shared algorithm for request processing, while actual work is performed by configurable delegate components. This model is flexible and supports diverse workflows.

说了这么半天,想必大家也看累了。既然MVC框架这么厉害,也吹了这么久 总得实现一下看看效果是不是

(SpringMVC有哪些特点,使用SpringMVC有什么好处,这边就不再赘述了 网上文章也很多,再写下去也没什么意义。)

实现一个简单的MVC程序

下面的方式是比较老的MVC实现方式,在真实的开发中几乎不会这么写。但使用这种方式来理解MVC初始化流程还是可取的

  • 创建maven项目
  • 导入jar包依赖
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.2.8.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>jsp-api</artifactId>
            <version>2.2</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet.jsp.jstl</groupId>
            <artifactId>jstl-api</artifactId>
            <version>1.2</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
        </dependency>
    </dependencies>
  • 添加web框架支持

  • 在web.xml中编写DispatcherServlet
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">   
    <servlet>
        <!--编写DispatcherServlet-->
        <servlet-name>Springmvc-Servlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <!--绑定Spring配置文件-->
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:applicationContext.xml</param-value>
        </init-param>
        <!--设置启动级别为 1 -->
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>Springmvc-Servlet</servlet-name>
        <!--拦截所有请求-->
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>
  • 从上面可以看到需要一个Spring配置文件供其绑定
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--映射器,该映射器会通过请求的url和BeanName来匹配对应的Handler-->
    <bean id="handlerMapping" class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>
    <!--处理器-->
    <bean id="handlerAdapter" class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"/>
    <!--视图解析器-->
    <bean id="resolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!--解析器拼接的视图前缀-->
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <!--解析器拼接的视图后缀-->
        <property name="suffix" value=".jsp"/>
    </bean>
    <!--注册一个 Controller供其映射-->
    <bean id="/hello" class="com.molu.controller.MvcController"/>
</beans>
  • 一环扣一环,又需要编写Controller类

编写Controller 有两种方式,一是实现Controller接口、二是在类上使用Controller注解。注解使用的更多,前者相反。

但为了解释原理我们还是通过实现接口的方式

// 继承 Controller接口
public class MvcController implements Controller {
    //重写Controller接口中的方法,该方法需要返回一个ModelAndView
    @Override
    public ModelAndView handleRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
        // new一个 ModelAndView
        ModelAndView modelAndView = new ModelAndView();
        // 往 ModelAndView中封装数据
        modelAndView.addObject("msg","Hello MVC!");
        // 往 ModelAndView中封装一个视图名称
        modelAndView.setViewName("hello");
        // 返回封装好的 ModelAndView
        return modelAndView;
    }
}
  • 既然封装了视图名称,对应的我们就需要一个视图。

编写hello.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<%--几乎不进行其他操作,只需要取到封装的数据(msg)即可--%>
${msg}
</body>
</html>
  • 到这里该写的都写完了,也没几行代码。Spring都给我把需要准备的准备好了。
  • 启动Tomcat,在浏览器中进行请求

很简单不是吗,而我们这种实现方式 实际上还算比较繁杂了。使用注解的话我们还能够更偷懒。

注解在下面会讲,如果你是一个初学者,只是将这些代码敲一遍并不能理解其初始化流程做了什么。

SpringMVC初始化流程

  • 我们向浏览器发起请求(url)

  • DispatcherServlet接收该请求(url)并通过调用Handler Mapping(处理器映射器)拿到对应的Handler(处理器),并生成处理器执行链HandlerExecutionChain(包括处理器对象和处理器拦截器),将其返回给DispatcherServlet

    • 也就是通过下面这行代码拿到对应的Controller
        <!--注册一个 Controller供其映射-->
        <bean id="/hello" class="com.molu.controller.MvcController"/>
    
  • DispatcherServlet 根据处理器,得到对应的处理器适配器(HandlerAdapter)。

  • 适配器会按照特定的规则执行Handler,Handler将执行具体的Controller

  • Cotroller会将执行的结果(比如 ModelAndView)交给HandelerAdapter

  • HandlerAdapter将其结果返回至DispatcherServlet。

  • DispatcherServlet会将该返回结果(MAV)传给视图解析器(ViewReslover)。

  • 视图解析器则会解析MAV(ModelAndView)中的数据,拿到我们封装在MAV中的视图名称

    • modelAndView.setViewName("hello");
      
  • 视图解析器对该名称进行拼接操作,并将拼接好的具体视图交给DispatcherServlet

    • /WEB-INF/jsp/hello.jsp
      
    •     <!--视图解析器-->
          <bean id="resolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
              <!--解析器拼接的视图前缀-->
              <property name="prefix" value="/WEB-INF/jsp/"/>
              <!--解析器拼接的视图后缀-->
              <property name="suffix" value=".jsp"/>
          </bean>
      
  • DispatcherServlet会对具体的视图进行渲染操作,也就是将模型数据model填充至视图中。

  • DispatcherServlet将渲染好的视图返回给客户端(也就是我们看到的浏览器页面)。

组件说明:

1.DispatcherServlet:前端控制器。用户请求到达前端控制器,它就相当于mvc模式中的c,dispatcherServlet是整个流程控制的中心,由它调用其它组件处理用户的请求,dispatcherServlet的存在降低了组件之间的耦合性,系统扩展性提高。由框架实现 2.HandlerMapping:处理器映射器。HandlerMapping负责根据用户请求的url找到Handler即处理器,springmvc提供了不同的映射器实现不同的映射方式,根据一定的规则去查找,例如:xml配置方式,实现接口方式,注解方式等。由框架实现 3.Handler:处理器。Handler 是继DispatcherServlet前端控制器的后端控制器,在DispatcherServlet的控制下Handler对具体的用户请求进行处理。由于Handler涉及到具体的用户业务请求,所以一般情况需要程序员根据业务需求开发Handler。 4.HandlAdapter:处理器适配器。通过HandlerAdapter对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理器进行执行。由框架实现。 5.ModelAndView是springmvc的封装对象,将model和view封装在一起。 6.ViewResolver:视图解析器。ViewResolver负责将处理结果生成View视图,ViewResolver首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成View视图对象,最后对View进行渲染将处理结果通过页面展示给用户。 7View:是springmvc的封装对象,是一个接口, springmvc框架提供了很多的View视图类型,包括:jspview,pdfview,jstlView、freemarkerView、pdfView等。一般情况下需要通过页面标签或页面模版技术将模型数据通过页面展示给用户,需要由程序员根据业务需求开发具体的页面。

可能看完这些你仍是一脸懵,SpringMVC帮我们做了很多操作,这些操作都看不到。

不能够理解也很正常,但随着你对MVC的深入这些执行流程也就自通了。也没必要刚接触就去深究其执行流程。

能理解自然是最好,不能理解也没什么关系,懂得如何实现就好。当然所谓的实现 并不是通过上面这种老掉牙的方式实现。

注解实现MVC操作

注解实现MVC操作更加的便捷,需要我们写的东西也更少。

相对的黑盒操作也就更多,如果对执行流程完全不理解,但会写注解就有点知其然不知其所以然了。

所以最好还是能够理解一些执行流程,起码要知道自己写的注解和配置到底写的是哪部分。

  • Dispatcher Servlet部分不进行任何改动
  • 修改Spring配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       https://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/mvc
       http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!--
    配置自动扫描包,该包下的注解会生效。
    使用注解的类会被Spring装配
    -->
    <context:component-scan base-package="com.molu.controller"/>

    <!--
    配置MVC注解驱动
    配置以后我们不需要显示的声明 处理器映射器和处理器适配器
    annotation-driven配置帮助我们自动完成上述两个实例的注入
    -->
    <mvc:annotation-driven/>

    <!--配置资源过滤,让SpringMVC不处理静态资源-->
    <mvc:default-servlet-handler/>

    <!--配置视图解析器-->
    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!--前缀-->
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <!--后缀-->
        <property name="suffix" value=".jsp"/>
    </bean>
</beans>

可以看到,使用注解后 我们就不再需要显示的定义HandlerMapping和HandlerAdapter了。

使用一个 <mvc:annotation-driven/>配置就能够搞定。

  • 对Controller类进行修改
// 使用 Controller注解使该类拥有处理器的功能,且该类会被Spring装配。
@Controller
public class MvcController {

    // 使用 RequestMapping来完成请求和Handler的映射
    @RequestMapping("/hello")
    // 写一个简单的方法(返回值为 String)
    public String helloMVC(Model model){
        // 使用 model封装数据
        model.addAttribute("msg","Hello MVC!");
        // 返回的字符串会被视图解析器处理,也就是说我们返回的字符串就是视图名称。
        return "hello";
    }
}

使用注解后,Controller类也变得更加精简。

使用注解后我们的Spring配置文件就不再进行改动了,不存在写一个 Controller类 就要去配置文件中注册一个Bean的操作。

且使用注解后,我们一个类中可以有多个Controller(一个方法对应一个Controller,我们可以在类中写多个方法)。

而使用实现接口的方式,则无法在一个类中写多个Controller,这也算注解的一些优势。

  • @Controller

    @Controller注解用于声明 Spring类的实例,用@Controller声明的类就是一个控制器,且会被Spring装配。 使用该注解类的所有方法,如果返回值是 String并且有对应的视图可以跳转,就会被视图解析器解析。

  • @RequestMapping

    @RequestMapping 注解的value值,用于指定请求和Handler的映射。 如果在类上使用该注解,在请求时url就会多一层父关系。

    比方说我在类上使用 @RequestMapping注解,注解的Value值为("/hello"),类中的方法上再写一个 @RequestMapping注解,注解的Value值为("/h1")。

    那么请求的url地址应该会是这样http://localhost:8080/hello/h1 “。

关于注解的更多操作也就不在这里过多赘述了,感兴趣可以移步其他博客

转发与重定向

在SpringMVC中,我们除了使用Servlet原生的API来实现转发和重定向操作,我们也可以使用以下几种方式。

有视图解析器的情况下

@Controller
public class MvcController {
    @RequestMapping("/hello")
    public String helloMVC(Model model){
        model.addAttribute("msg","Hello,MVC");
        return "hello";
    }
}

在使用了视图解析器后,我们 return的字符串默认就是走的转发。

验证

我们在 /WEB-INF/jsp/ 底下再写一个 test.jsp

并将原本的 helloMVC方法进行一些简单的修改

@Controller
public class MvcController {
    @RequestMapping("/hello")
    public String helloMVC(Model model){
        model.addAttribute("msg","执行了转发操作");
        return "test";
    }
}

在浏览器中仍然请求/hello

请求的路径没有任何变化,但视图已经变成 test.jsp了,很明显是走的转发操作。

接着我们来说说重定向

重定向的话也很简单,我们只需要在return的字符串前面加上一个redirect:即可

验证

对 return的字符串进行简单的修改

@Controller
public class MvcController {
    @RequestMapping("/hello")
    public String helloMVC(Model model){
        model.addAttribute("msg","执行了重定向操作");
        return "redirect:index.jsp";
    }
}

接着在浏览器中请求/hello

可以看到,很明显走了一个重定向的操作

没有视图解析器的情况

@Controller
public class MvcController {
    @RequestMapping("/hello")
    public String helloMVC(Model model){
        model.addAttribute("msg","执行了转发操作");
        return "/WEB-INF/jsp/test.jsp";
    }
}

没有视图解析器的情况下,我们则需要写全限定名。

你也可以在前面写一个 forward:,效果都是一样的

@Controller
public class MvcController {
    @RequestMapping("/hello")
    public String helloMVC(Model model){
        model.addAttribute("msg","执行了转发操作");
        return "forward:/WEB-INF/jsp/test.jsp";
    }
}

重定向也是一样,直接在前面加上一个redirect:,不赘述,感兴趣可以自己试一下。

接收参数和参数返回

我们来看一下在SpringMVC中如何接收前端的参数

在之前的web开发中,我们一般都使用Servlet的req resp来接收和返回参数,使用SpringMVC后,这一操作得到了简化。

首先我们修改一下我们的Controller类,在方法的参数中加一个 name。

@Controller
@RequestMapping("/user")
public class UserController {

    @RequestMapping("/test")
    public String test(String name, Model model){
        // 接收前端的参数
        System.out.println("前端输入的用户名为:" + name);
        // 返回前端传过来的参数
        model.addAttribute("msg","你输入的用户名为:"+ name);
        return "test";
    }
}

我们在浏览器中进行测试

没有问题,通过 ” ?“ 确实能进行一些简单传参数。后端也成功拿到了我们通过浏览器传递的参数。

这个时候如果我们将name修改为username,和后端的参数名不一致,会如何呢 。

当然了,也不是没有解决的方法,我们可以通过注解来进行标注。

我们只需要在方法的参数中加上一个 @RequestParam就能够解决前端传递的参数名和后端方法中的参数名不一致的问题。

public String test(@RequestParam("username") String name, Model model)

还有一种情况需要考虑,如果前端传递的是一个对象而不是属性,该怎么接收呢?

  • 写一个实体类
public class User {
    private int id;
    private String name;
    private int age;
// 以下省略构造方法和getter、setter、toString方法。
  • 对test方法进行简单的修改
@Controller
@RequestMapping("/user")
public class UserController {

    @RequestMapping("/test")
    public String test( User user, Model model){
        System.out.println("前端输入的用户是:" + user);

        HashMap map = new HashMap();
        map.put("id",user.getId());
        map.put("name",user.getName());
        map.put("age",user.getAge());
        model.addAllAttributes(map);
        model.addAttribute("msg","你输入的用户信息为:" + map);
        return "test";
    }
}

再次在浏览器中进行测试,但这一次是通过 ?传入User对象。

测试:通过url传入的对象属性和 对象中的属性不一致

到这里关于SpringMVC也就告一段落了,如果有帮到你的话。


放松下眼睛

原图P站地址

画师主页