Spring MVC详解

起源

书中用了近二十几页来讲解了MVC的起源。简单来讲,JAVA推出了Servlet之后,程序员觉得很好,导致Servlet在代码中滥用。SUN公司看不下去了,就推出了JSP,还指定了一些规范。但是制定歪了,该重点解决的还是没解决,然后JSP又滥用了。最后Servlet和JSP进行了联盟,推出了MVC的雏形。于是,Web开发也正式迈入了MVC的时代。

image-20200513161449922.png

概述

SpringMVC 是一种基于 Java 的实现 MVC 设计模型的请求驱动类型的轻量级 Web 框架,属于 Spring FrameWork 的后续产品,已经融合在 Spring Web Flow 里面。Spring 框架提供了构建 Web 应用程序的全功能 MVC 模块。使用 Spring 可插入的 MVC 架构,从而在使用 Spring 进行 WEB 开发时,可以选择使用 Spring的 Spring MVC 框架或集成其他 MVC 开发框架,如 Struts2 等。

SpringMVC 已经成为目前最主流的 MVC 框架之一,并且随着 Spring3.0 的发布,全面超越 Struts2,成为最优秀的 MVC 框架。

它通过一套注解,让一个简单的 Java 类成为处理请求的控制器,而无须实现任何接口。同时它还支持RESTful 编程风格的请求。

Spring MVC作用于经典三层架构的Web层,名字虽然有MVC,但是是实现 MVC 设计模型才叫这名字。不要把经典三层架构和MVC混为一谈。

另外,Spring MVC是请求驱动的Web框架底层离不开原始 Servlet API。

原理

Spring MVC有一个总的前端控制器DispatcherServlet,用于将Servlet统一进行管理,在启动时会创建一个Spring容器,即将需要加载的类全部加载进去。然后通过在类或者方法上配置的url路径,根据路径找到相应的方法执行。执行完以后,将相应的结果返回给游览器。(实现就这么简单,毕竟有个Spring这么牛逼的爹)

入门Demo

那么废话不多说了,直接用Demo来讲吧。书中的2.x版本依然没用,用的5版本的。

配置

引包

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.1.8.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.1.8.RELEASE</version>
</dependency>

web.xml中的配置

<servlet>
    <servlet-name>springmvc</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <!--负责启动spring容器 这个配置就是springmvc的配置位置-->
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:springmvc.xml</param-value>
    </init-param>
    <!--前端控制器启动时就把容器创建出来,不然则是等到第一次访问时创建容器-->
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>springmvc</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

springmvc.xml中的配置(暂时就这么一条)

<context:component-scan base-package="com.chayedan.web"></context:component-scan>

实现类

@Controller
@RequestMapping("/user")
public class UserController {
    /**
     * 注解 的含义 就是该方法 准备映射某个访问路径
     */
    @RequestMapping("/test1")
    public ModelAndView test1(ModelAndView modelAndView){
        Date date = new Date();
        String content="当前时间:"+date.toLocaleString();
        //类似于写 request.setAttribute("属性名",属性值)
        modelAndView.addObject("content",content);
        //等同于 request.getDispatcher("").forward(request,response)
        modelAndView.setViewName("/WEB-INF/user/time.jsp");
        return modelAndView;
    }
}

主要还是@RequestMapping来映射路径

还有一个强大的地方就是自动帮你封装对象了

@RequestMapping("/test2")
public ModelAndView test2(ModelAndView modelAndView, User user){
    System.out.println(user);

    Date date = new Date();
    String content="当前时间:"+date.toLocaleString();
    //类似于写 request.setAttribute("属性名",属性值)
    modelAndView.addObject("content",content);
    //等同于 request.getDispatcher("").forward(request,response)
    modelAndView.setViewName("/WEB-INF/user/time.jsp");
    return modelAndView;
}

比如说我User有id和name属性,那么我通过URL传参时,会自动帮我封装为一个User对象。只要你是正常传参的。

备注

参数绑定

如果不能正常的进行参数绑定,请打开你的IDE,例如我是用的IDEA

image-20200514110452725.png

看这个选项是否勾选上,没有勾选时,javac编译出来的class文件中的方法参数列表中是不带参数名的。比如说

void add(int a)----->void add(int)

会变成上面那样

但是也可以使用@RequestParam进行绑定,这样勾不勾那个选项都无所谓了,比如

void add(@RequestParam(name="a") int a)

Tomcat路径匹配

上面web.xml的第14行其实是有问题的,tomcat的匹配过程中,是按照精确匹配,目录匹配,后缀名匹配,最后是/匹配。而上面那个的意思是由Spring MVC来处理全部的请求。但我们这里还没写所有的请求的处理方法,比如说请求一个/static/hello.html,就会出现404。

如果要加载需要手动加上静态文件的后缀名,交给tomcat自带的defaultServlet去处理

<servlet-mapping>
        <!--放行的类型-->
        <servlet-name>default</servlet-name>
        <url-pattern>*.html</url-pattern>
        <url-pattern>*.css</url-pattern>
        <url-pattern>*.js</url-pattern>
        <url-pattern>*.png</url-pattern>
        <url-pattern>*.jpg</url-pattern>
</servlet-mapping>

不过我觉得还是先不管吧,写*.do啊什么的也没什么不好

编码问题

中文乱码用过滤器解决,在web.xml中

<filter>
        <filter-name>encodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>utf-8</param-value>
        </init-param>
</filter>
<!--  所有被过滤后都进行字节码编码(防中文乱码)    -->
<filter-mapping>
        <filter-name>encodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
</filter-mapping>

Spring MVC的组件

先看看具体的解析过程

image-20200513213155982.png

DispatcherServlet:用户请求到达前端控制器,它就相当于 mvc 模式中的 c,dispatcherServlet 是整个流程控制的中心,由它调用其它组件处理用户的请求,dispatcherServlet 的存在降低了组件之间的耦合性。

HandlerMapping:负责根据用户请求找到 Handler 即处理器,SpringMVC 提供了不同的映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。

Handler :它就是我们开发中要编写的具体业务控制器。由 DispatcherServlet 把用户请求转发到 Handler。由Handler 对具体的用户请求进行处理。

HandlAdapter:通过处理器适配器对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理器进行执行。

View Resolver:View Resolver 负责将处理结果生成 View 视图,View Resolver 首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成 View 视图对象,最后对 View 进行渲染将处理结果通过页面展示给用户。

在 SpringMVC 的各个组件中,处理器映射器、处理器适配器、视图解析器称为 SpringMVC 的三大组件。

备注

5.0以前的版本,处理映射器(比如说常用的RequestMapping)跟5.0的默认不同,需要在springmvc.xml中增加

<mvc:annotation-driven ></mvc:annotation-driven>

常用功能详解

@RequestMapping

源码

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
    String name() default "";

    @AliasFor("path")
    String[] value() default {};

    @AliasFor("value")
    String[] path() default {};

    RequestMethod[] method() default {};

    String[] params() default {};

    String[] headers() default {};

    String[] consumes() default {};

    String[] produces() default {};
}

主要还是用到下面几个属性

  • value:用于指定请求的 URL。它和 path 属性的作用是一样的。
  • method:用于指定请求的方式。
  • params:用于指定限制请求参数的条件。支持简单的表达式。要求请求参数的 key 和 value 必须和配置的一模一样。

比如像下面这样

@RequestMapping(value="/saveAccount",method=RequestMethod.POST)

表明必须用POST方式来请求这个方法

@RequestMapping(value="/removeAccount",params="username")

表明请求时必须带上username参数

@RequestBody

在Spring MVC中是无法自动进行json格式的相互转换的,需要借助第三方,如借助jackson

springmvc.xml中配置

<mvc:annotation-driven>
    <mvc:message-converters>
        <!--
            进行一下jackson转换
         -->
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"></bean>
    </mvc:message-converters>
</mvc:annotation-driven>

那么回到正题

这个注释就是将发过来的json数据封装为javaBean。如下使用即可

public String test(@RequestBody(required=false) User user)

属性required:是否必须有请求体。默认值是:true。当取值为 true 时,get 请求方式会报错。如果取值为 false,get 请求得到是 null。一般不写,这里只是写出来看一下

@ResponseBody

该注解用于将 Controller 的方法返回的对象,通过 HttpMessageConverter 接口转换为指定格式的数据如:json,xml 等,通过 Response 响应给客户端。

也就是将需要放回的数据按照 JSON的格式返回给游览器。

@RestController

在这个类中的方法,等于全部加上了@ResponseBody

文件上传

这里使用第三方包来进行文件上传功能的实现

导包

<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.3.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.6</version>
</dependency>

添加配置

在springmvc.xml中添加上传的类

<!--
    指明文件上传解析类 
    bean的id不能够乱写,必须为这个名字
    因为springmvc根据名字找到解析类
-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <!--文件上传的最大限度,字节为单位-->
    <property name="maxUploadSize" value="10485760"/>
</bean>

DEMO

@Controller
public class UploadController {
    /*
    * MultipartFile 就是springmvc给我们封装好文件对象  
    * 参数名字必须和input输入框的name属性一致
    * */
    @RequestMapping("/upload")
    @ResponseBody
    public String upload(MultipartFile avatar) throws IOException {
        String contentType = avatar.getContentType();
        System.out.println("文件类型:"+contentType);
        String name = avatar.getName();
        System.out.println("代表是参数name的属性值:"+name);

        String originalFilename = avatar.getOriginalFilename();
        System.out.println("原本的文件名:"+originalFilename);
        
        return "success";
    }
}

前端的代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h3>文件上传</h3>
    <form method="post" action="/upload" enctype="multipart/form-data">
        用户名:<input type="text" name="username"><br>
        头像:<input type="file" name="avatar"><br>
        <input type="submit" value="点我提交">

    </form>
</body>
</html>

异常处理

如果在系统中出现了异常,异常可以通过throw最后到前端控制器,由前端控制器交给异常处理器(HandlerExceptionResolver)处理,Spring本身自带了,如果没有什么特别的要求,可以使用默认的,但是也可以来自定义,需要实现HandlerExceptionResolver接口。

不过没什么卵用啊感觉,出现异常了不都原地解决呢?

DEMO

自定义的异常类1

package com.chayedan.exception;

public class MyException1 extends Exception {
    private String code;
    private String desc;

    public MyException1() {
    }

    public MyException1(String code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }
}

自定义的异常类2

package com.chayedan.exception;

public class MyException2 extends Exception {
    private String code;
    private String desc;

    public MyException2() {
    }

    public MyException2(String code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }
}

自定义的异常处理器对象

public class MyExceptionResolver implements HandlerExceptionResolver {

    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        ModelAndView modelAndView = new ModelAndView();

        if (ex instanceof MyException1){
            MyException1 exception1= (MyException1) ex;
            modelAndView.addObject("code",exception1.getCode());
            modelAndView.addObject("msg",exception1.getDesc());
        }else if(ex instanceof MyException2){
            MyException2 exception2= (MyException2) ex;
            modelAndView.addObject("code",exception2.getCode());
            modelAndView.addObject("msg",exception2.getDesc());
        }else{
            modelAndView.addObject("code","40404");
            modelAndView.addObject("msg","系统异常");
        }

    }
}

配置自定义的异常处理器

<!--注入异常解析器-->
<bean id="myexception" class="com.chayedan.web.MyExceptionResolver"></bean>