《学会 SpringMVC 系列 · 基础篇》

📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗

🌻 CSDN入驻不久,希望大家多多支持,后续会继续提升文章质量,绝不滥竽充数,欢迎多多交流。👍

文章目录

写在前面的话

博主所在公司早期后端采用 SSM(Spring + SpringMVC + MyBatis),2020年进行了微服务拆分,顺带技术栈升级,后端调整为 SpringCloud 和 SpringBoot,深刻体会到了SpringBoot为我们带来的遍历。

经常会听到新人之间在议论,"还是新框架好,旧框架已过时了,不要浪费时间学习旧框架",那么 SpringBoot 是否完全取代了 SSM 技术栈呢?

那么,有了 SpringBoot,是否还需要学习 SpringMVC?

毋庸置疑,SpringBoot 的出现确实简化了 SpringMVC 的配置,但 SpringMVC 作为 Spring 生态中重要的组成部分,其底层原理和核心概念依然值得深入学习。

  • 更深入理解:SpringBoot 虽然简化了配置,但它本质上还是基于 SpringMVC 的。深入理解 SpringMVC 可以让你更好地掌握 SpringBoot 的底层原理,从而更灵活地解决问题。
  • 定制化开发:SpringBoot 提供了大量的自动配置,但有时我们可能需要进行高度定制化的开发。这时,对 SpringMVC 的底层原理的掌握就显得尤为重要。
  • 面试:很多公司的面试都会考察 SpringMVC 的基础知识,深入了解 SpringMVC 可以帮助你更好地应对面试。
  • 旧项目维护:如果需要维护一些基于 SpringMVC 的老旧项目,了解 SpringMVC 的知识也是必不可少的。

总结一下:

SpringBoot 和 SpringMVC 并不是对立的关系,而是相辅相成的。初学者建议优先学习 SpringBoot,可以让你快速上手,提高开发效率。接着深入学习 SpringMVC 的底层原理,更深入得使用其定制化能力,从而写出更高质量的代码。

接下来的本系列文章,将从 SpringMVC 的搭建、常见用法、源码分析、扩展点分析、企业实战等方面展开。

好,先开始基础篇的介绍!


搭建 SpringMVC

Step1、创建 Maven 项目

开发环境:

IDE:IDEA 2022

构建工具:Maven 3.x

服务器:Tomcat 8

Spring版本:5.3.15

创建 Maven 项目:

使用 IDEA 创建一个Maven 子模块, 可以使用模板方式创建剩下不少功夫,具体如下图:

先展示Demo全貌:

圈出来的文件也是下面几个步骤需要交互的,后续就不一一截图了。


Step2、引入 Maven 依赖

xml 复制代码
<dependencies>
  
  <!-- SpringMVC -->
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.3.15</version>
  </dependency>
  
  <!-- 日志 -->
  <dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
  </dependency>
  
  <!-- ServletAPI -->
  <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <scope>provided</scope>
  </dependency>
  
  <!-- Spring5和Thymeleaf整合包 -->
  <dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf-spring5</artifactId>
    <version>3.0.12.RELEASE</version>
  </dependency>

</dependencies>

引入后查看 Maven 依赖:


Step3、配置 web.xml

【知识扫盲】

web.xml 是一个传统的 Java Web 应用的部署描述文件,它用于配置 Web 应用的一些基本信息,比如:监听器、过滤器、过滤器、Servlet、欢迎页等。

在 Spring MVC 项目中,web.xml 主要负责:

1、加载 Spring 容器,通过 ContextLoaderListener 监听器加载 Spring 配置文件(applicationContext.xml 等),初始化 Spring 容器。

2、配置前端控制器 DispatcherServlet,DispatcherServlet 是 Spring MVC 的核心,负责拦截请求,分发给相应的 Controller 处理。

Tomcat 容器启动的时候,会加载 web.xml,这是一个复杂的过程,这边先不展开。

web.xml 内部元素的加载顺序通常是:context-param -> listener -> filter -> servlet。

值得一提的是,Spring Boot 内嵌了 Tomcat,不需要额外的部署描述文件。

【关键代码】

xml 复制代码
<!-- 配置SpringMVC的前端控制器,对浏览器发送的请求统一进行处理 -->
<servlet>
    <servlet-name>springMVC</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!-- 通过初始化参数指定SpringMVC配置文件的位置和名称 -->
    <init-param>
        <!-- contextConfigLocation为固定值 -->
        <param-name>contextConfigLocation</param-name>
        <!-- 使用classpath:表示从类路径查找配置文件,例如maven工程中的src/main/resources -->
        <param-value>classpath:spring-mvc.xml</param-value>
    </init-param>
    <!--
         作为框架的核心组件,在启动过程中有大量的初始化操作要做
        而这些操作放在第一次请求时才执行会严重影响访问速度
        因此需要通过此标签将启动控制DispatcherServlet的初始化时间提前到服务器启动时
    -->
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>springMVC</servlet-name>
    <!--
        设置springMVC的核心控制器所能处理的请求的请求路径
        /所匹配的请求可以是/login或.html或.js或.css方式的请求路径
        但是/不能匹配.jsp请求路径的请求
    -->
    <url-pattern>/</url-pattern>
</servlet-mapping>

Step4、创建 spring-mvc.xml

web.xml里面加载的配置文件,当然也可以通过添加配置类的形式实现功能,这里就不展开了。

xml 复制代码
<?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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">

    <!-- 自动扫描包 -->
    <context:component-scan base-package="com.lw.mvc.web"/>
  
</beans>

Step5、编写控制器类

如下所示,一个方法返回视图,一个方法返回数据。

Tips:index.jsp是Maven模板自带的,如果要显示中文,注意加上编码设置。

Tips:目前基本都是前后端分离模式,因此主要关注返回数据的场景。

java 复制代码
@Controller
public class HelloController {

    @RequestMapping("/")
    public String index() {
        return "index";
    }

    @ResponseBody
    @RequestMapping(value = {"/data", "/test"})
    public String data() {
        return "12345";
    }
}
html 复制代码
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<html>
<body>
<h2>Hello 战神!</h2>
</body>
</html>

Step6、部署 Tomcat

这步比较简单,直接看图:

点击启动后,会自动访问:http://localhost:8085/study_mvc/

修改访问地址:http://localhost:8085/study_mvc/data

也可以正常输出数据,搞定收工!


搭建 SpringMVC 复盘

JSON 类型数据传递

接下来,测试一下JSON类型数据的传递。

先定义一个实体类,并修改控制层代码,@RequestBody 接收参数,@ResponseBody 响应结果,如下所示。

java 复制代码
public class Student implements Serializable {

    private static final long serialVersionUID = 3227472127782930834L;
    private Integer id;
    private String name;
    private String email;
    private Integer age;

    ...
}

@Controller
public class HelloController {

    @ResponseBody
    @RequestMapping(value = "stu")
    public Student stu(@RequestBody Student student) {
        student.setName("战神");
        return student;
    }
}

启动代码试试看,Postman测试如下:

解决方案也简单,引入一下依赖:

xml 复制代码
<!-- jackson -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.0</version>
</dependency>

重启一下服务,再试试:

Tips:很多文章介绍需要额外的步骤,其实引入依赖才是核心。


关于静态资源处理

背景前提:

如果你的 DispatcherServlet 拦截的是 *.do 这样的URL,就不存在访问不到静态资源的问题,但要多书写.do

如果你的 DispatcherServlet 拦截的是 /,代表拦截了所有的请求,同时对 .js、 .jpg 的访问也就被拦截了。

但 DispatcherServlet 不具备静态文件的处理能力,所以需要额外的配置。

映射规则参考一下知识延伸 - url-pattern 映射规则

目前有三种方案,那么如何选择,主要需求是可以访问静态文件,不要报404,当然效率也要注意。

方案一:激活Tomcat的defaultServlet来处理静态文件(推荐)

xml 复制代码
<servlet-mapping>
  <servlet-name>default</servlet-name>
  <url-pattern>*.jpg</url-pattern>
  <url-pattern>*.gif</url-pattern>
  <url-pattern>*.png</url-pattern>
  <url-pattern>*.js</url-pattern>
  <url-pattern>*.css</url-pattern>
  <url-pattern>*.html</url-pattern>
</servlet-mapping> 

说明:要配置多个,每种文件配置一个,并且要写在 DispatcherServlet 的前面,让 defaultServlet 先拦截,这样就不会进入Spring了,我想性能是最好的吧。

补充说明:

这里的 default,其实是在$tomcat/conf/web.xml文件中配置的:

该 web.xml 的执行优先级低于项目自带的 web.xml,但如果项目的没注册这个Servlet,则以 Tomcat 的为准。

xml 复制代码
<servlet>
  <servlet-name>default</servlet-name>
  <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
  <init-param>
    <param-name>debug</param-name>
    <param-value>0</param-value>
  </init-param>
  <init-param>
    <param-name>listings</param-name>
    <param-value>false</param-value>
  </init-param>
  <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
  <servlet-name>default</servlet-name>
  <url-pattern>/</url-pattern>
</servlet-mapping>

如果某个 Servlet 的映射路径仅仅是一个/,那么这个Servlet就成为当前web应用的默认Servlet它可以处理其它所有Servlet都不处理的请求。开发时最好不要出现这种情况,否则web应用的静态资源无法被访问,从而被此Servlet 拦截处理。

xml 复制代码
 <!-- ================配置SpringMVC核心调度器================ -->
<servlet>
    <servlet-name>SpringMVC</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath*:mvc-config.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>

这种情况会覆盖由 tomcat 提供的默认的 Servlet,该Serlvet是为静态资源提供访问服务的。

所有要额外配置其他高优先级处理器,也就是按方案一的设置,指定类型的文件,访问的时候,先使用默认Servlet处理,找不到才使用 MVC 的。

方案二: 在spring3.0.4以后版本提供了mvc:resources **
mvc:resources 的使用方法:
<mvc:resources location="/images/" mapping="/images/
"/>

<mvc:resources location="/js/" mapping="/js/"/>
<mvc:resources location="/css/" mapping="/css/
"/>

/images/**映射到 ResourceHttpRequestHandler进行处理,location指定静态资源的位置,可以是web application根目录下、jar包里面,这样可以把静态资源压缩到jar包中。cache-period 可以使得静态资源进行web cache

如果出现下面的错误,可能是没有配置<mvc:annotation-driven />的原因。

报错WARNING: No mapping found for HTTP request with URI [/mvc/user/findUser/lisi/770] in DispatcherServlet with name 'springMVC'

使用mvc:resources/元素,把mapping的URI注册到SimpleUrlHandlerMapping的urlMap中,key为mapping的URI pattern值,而value为ResourceHttpRequestHandler,

这样就巧妙的把对静态资源的访问由HandlerMapping转到ResourceHttpRequestHandler处理并返回,所以就支持classpath目录,jar包内静态资源的访问。

另外需要注意的一点是,不要对SimpleUrlHandlerMapping设置defaultHandler.因为对static uri的defaultHandler就是ResourceHttpRequestHandler,

否则无法处理static resources request。

方案三,使用mvc:default-servlet-handler/

Xml代码 :mvc:default-servlet-handler/

会把"/**" url 注册到SimpleUrlHandlerMapping的urlMap中,把对静态资源的访问由HandlerMapping转到 org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler 处理并返回。

DefaultServletHttpRequestHandler使用就是各个Servlet容器自己的默认Servlet。

需要搭配: mvc:annotation-driven/

补充说明1:

多个HandlerMapping的执行顺序问题:

DefaultAnnotationHandlerMapping的order属性值是:0

<mvc:resources/ >自动注册的 SimpleUrlHandlerMapping的order属性值是: 2147483646

mvc:default-servlet-handler/自动注册的SimpleUrlHandlerMapping的order属性值是:2147483647

spring会先执行order值比较小的。当访问一个a.jpg图片文件时,先通过DefaultAnnotationHandlerMapping 来找处理器,一定是找不到的,我们没有叫a.jpg的Action。再按order值升序找,由于最后一个SimpleUrlHandlerMapping 是匹配"/**"的,所以一定会匹配上,再响应图片。

补充说明2:

访问一个图片,还要走层层匹配。真不知性能如何?改天做一下压力测试,与Apache比一比。

最后再说明一下,如何你的DispatcherServlet拦截 *.do这样的URL,就不存上述问题了。


对比 SpringBoot

玩过 SpringBoot 的人,看到上面步骤,一定感觉十分繁琐(对熟练的人另说)。

就像这篇《 搭建拥有数据交互的 SpringBoot 》介绍的,可能两三下就可以获取一个 SpringBoot 项目。

前面提到的,返回JSON数据也不需要额外的配置(依赖都导入了),静态资源也有默认约定。

果然还是便捷啊,不过还是前面说的,先使用 SpringBoot,再深入理解 SpringMVC。


知识补充

@RequestMapping

Tips:控制层 @Controller 的核心注解,设置接口的访问路径,类和方法上可以添加。

@RequestMapping 注解的功能

从注解名称上我们可以看到,@RequestMapping 注解的作用就是将请求和处理请求的控制器方法关联起来,建立映射关系。SpringMVC 接收到指定的请求,就会来找到在映射关系中对应的控制器方法来处理这个请求。

@RequestMapping 注解的位置

@RequestMapping 标识一个类:设置映射请求的请求路径的初始信息。

@RequestMapping 标识一个方法:设置映射请求请求路径的具体信息。

Tips:@Target({ElementType.TYPE, ElementType.METHOD})

@RequestMapping 注解的 value 属性

@RequestMapping 注解的 value 属性通过请求的请求地址匹配请求映射。

@RequestMapping 注解的 value 属性是一个字符串类型的数组,表示该请求映射能够匹配多个请求地址所对应的请求,通常数组类型的注解属性,值可以是如下两种形式:

java 复制代码
@RequestMapping(value = {"/data", "/test"})
@RequestMapping("/testParam")

@RequestMapping 注解的 value 属性必须设置,至少通过请求地址匹配请求映射

@RequestMapping 注解的 method 属性

method 属性通过请求的请求方式(get或post)匹配请求映射

method 属性是一个 RequestMethod 类型的数组,表示该请求映射能够匹配多种请求方式的请求

若当前请求的请求地址满足请求映射的value属性,但是请求方式不满足method属性,则浏览器报错405:Request method 'POST' not supported

写法如下:method = {RequestMethod.GET, RequestMethod.POST}

补充:@RequestMapping 的派生注解

对于处理指定请求方式的控制器方法,SpringMVC中提供了@RequestMapping的派生注解

处理get请求的映射-->@GetMapping

处理post请求的映射-->@PostMapping

处理put请求的映射-->@PutMapping

处理delete请求的映射-->@DeleteMapping

补充:常用的请求方式

常用的请求方式有 get,post,put,delete,但是目前浏览器只支持get和post,若在form表单提交时,为method 设置了其他请求方式的字符串(put或delete),则按照默认的请求方式get处理。

Tips:form 不支持发送这个method设置,但 ajax 和 axios 支持。

@RequestMapping 注解的 params属性

Tips:这个属性设置的多个值必须同时满足才能触发,本属性基本不用,了解即可。

params 属性通过请求的请求参数匹配请求映射,是一个字符串类型的数组,可以通过四种表达式设置请求参数和请求映射的匹配关系。

"param":要求请求映射所匹配的请求必须携带param请求参数

"!param":要求请求映射所匹配的请求必须不能携带param请求参数

"param=value":要求请求映射所匹配的请求必须携带param请求参数且param=value

"param!=value":要求请求映射所匹配的请求必须携带param请求参数但是param!=value

来一段Demo:

java 复制代码
@RequestMapping(
        value = {"/testRequestMapping", "/test"}
        ,method = {RequestMethod.GET, RequestMethod.POST}
        ,params = {"username","password!=123456"}
)

若当前请求满足 value和method属性,但不满足params属性,此时页面回报错400:

Parameter conditions "username, password!=123456" not met for actual request parameters: username={admin}, password={123456}

@RequestMapping 注解的 headers 属性

基本同上,用的很少!

headers 属性通过请求的请求头信息匹配请求映射是一个字符串类型的数组,可以通过四种表达式设置请求头信息和请求映射的匹配关系。

"header":要求请求映射所匹配的请求必须携带header请求头信息

"!header":要求请求映射所匹配的请求必须不能携带header请求头信息

"header=value":要求请求映射所匹配的请求必须携带header请求头信息且header=value

"header!=value":要求请求映射所匹配的请求必须携带header请求头信息且header!=value

若当前请求满足@RequestMapping注解的value和method属性,但是不满足headers属性,此时页面显示404错误,即资源未找到

SpringMVC 支持ant风格的路径

即模糊匹配用法

?:表示任意的单个字符,有且仅能1个,不能是"/"这类型特殊字符

*:表示任意的0个或多个字符
:表示任意的一层或多层目录,只能使用//xxx的方式

SpringMVC 支持路径中的占位符

原始方式:/deleteUser?id=1

rest方式:/deleteUser/1

SpringMVC 路径中的占位符常用于 RESTful 风格中,当请求路径中将某些数据通过路径的方式传输到服务器中,就可以在相应的 @RequestMapping 注解的 value 属性中通过占位符 {xxx} 表示传输的数据,再通过@PathVariable 注解,将占位符所表示的数据赋值给控制器方法的形参。

来一段Demo:

java 复制代码
///testRest/1/admin
//最终输出的内容为-->id:1,username:admin
@RequestMapping("/testRest/{id}/{username}")
public String testRest(@PathVariable("id") String id, @PathVariable("username") String username){
    System.out.println("id:"+id+",username:"+username);
    return "success";
}

获取请求参数

接收参数的方式,与请求传递的 ContentType 紧密有关,常见的有 application/x-www-form-urlencoded 和 application/json 两种类型,后续专栏介绍。

1、普通参数

可以使用 @RequestParam 注解接收,或者HttpServletRequest#getParameter接收,详细细节后续篇章介绍。

这里只能处理 x-www-form-urlencoded 类型的请求,get和post方式都可以,但 json 不行。

java 复制代码
@ResponseBody
@RequestMapping("/testParam")
public String testParam(HttpServletRequest request, String xxx, 
                        @RequestParam("xxx2") String xxx2) {
    System.out.println("xxx:" + xxx);
    System.out.println("xxx2:" + xxx2);
    String xxx3 = request.getParameter("xxx2");
    System.out.println("xxx3:" + xxx3);
    return "hello";
}

2、POJO 类型参数

这种情况,不能使用 @RequestParam 接收,直接写实体即可,效果如下。

这里是 x-www-form-urlencoded 的示例,如果是 json 请求,直接参考上面章节,使用 @RequestBody。

java 复制代码
@ResponseBody
@RequestMapping("/testParam2")
public Student testParam2(Student student) {
    student.setName("战神");
    return student;
}

Tips:SpringMVC会使用构造器实例化出一个pojo类对象,即Student,然后使用setXxx()方法进行赋值,如果没有构造器会报错,没有set方法那么这个成员变量就为空null值。

3、其他类型

还有数组、List、Map,以及相应的复杂结构,后续章节展开介绍。



报文信息转换

HttpMessageConverter,报文信息转换器,将请求报文转换为Java对象,或将Java对象转换为响应报文。

HttpMessageConverter提供了两个注解和两个类型:

@RequestBody,@ResponseBody,RequestEntity,ResponseEntity

@RequestBody

@RequestBody 可以获取请求体,需要在控制器方法设置一个形参,使用@RequestBody进行标识,当前请求的请求体就会为当前注解所标识的形参赋值。

html 复制代码
<form th:action="@{/testRequestBody}" method="post">
  用户名:<input type="text" name="username"><br>
  密码:<input type="password" name="password"><br>
  <input type="submit">
</form>
java 复制代码
// 输出结果:requestBody:username=admin&password=123456
@RequestMapping("/testRequestBody")
public String testRequestBody(@RequestBody String requestBody){
    System.out.println("requestBody:"+requestBody);
    return "success";
}

RequestEntity

RequestEntity封装请求报文的一种类型,需要在控制器方法的形参中设置该类型的形参,当前请求的请求报文就会赋值给该形参,可以通过getHeaders()获取请求头信息,通过getBody()获取请求体信息。

java 复制代码
@RequestMapping("/testRequestEntity")
public String testRequestEntity(RequestEntity<String> requestEntity){
    System.out.println("requestHeader:"+requestEntity.getHeaders());
    System.out.println("requestBody:"+requestEntity.getBody());
    return "success";
}

输出结果:

requestHeader:[host:"localhost:8080", connection:"keep-alive", content-length:"27", cache-control:"max-age=0", sec-ch-ua:"" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"", sec-ch-ua-mobile:"?0", upgrade-insecure-requests:"1", origin:"http://localhost:8080", user-agent:"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36"]

requestBody:username=admin&password=123

@ResponseBody

@ResponseBody用于标识一个控制器方法,可以将该方法的返回值直接作为响应报文的响应体响应到浏览器

@ResponseBody 处理 json

a>导入jackson的依赖

xml 复制代码
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.12.1</version>
</dependency>

b>在SpringMVC的核心配置文件中开启mvc的注解驱动,此时在HandlerAdaptor中会自动装配一个消息转换器:MappingJackson2HttpMessageConverter,可以将响应到浏览器的Java对象转换为Json格式的字符串

<mvc:annotation-driven />

c>在处理器方法上使用@ResponseBody注解进行标识

d>将Java对象直接作为控制器方法的返回值返回,就会自动转换为Json格式的字符串

@RestController注解

@RestController注解是springMVC提供的一个复合注解,标识在控制器的类上,就相当于为类添加了@Controller注解,并且为其中的每个方法添加了@ResponseBody注解。

ResponseEntity

ResponseEntity 用于控制器方法的返回值类型,该控制器方法的返回值就是响应到浏览器的响应报文。


文件上传下载

使用 ResponseEntity 实现下载文件的功能

java 复制代码
@RequestMapping("/testDown")
public ResponseEntity<byte[]> testResponseEntity(HttpSession session) throws IOException {
    //获取ServletContext对象
    ServletContext servletContext = session.getServletContext();
    //获取服务器中文件的真实路径
    String realPath = servletContext.getRealPath("/static/img/1.jpg");
    //创建输入流
    InputStream is = new FileInputStream(realPath);
    //创建字节数组
    byte[] bytes = new byte[is.available()];
    //将流读到字节数组中
    is.read(bytes);
    //创建HttpHeaders对象设置响应头信息
    MultiValueMap<String, String> headers = new HttpHeaders();
    //设置要下载方式以及下载文件的名字
    headers.add("Content-Disposition", "attachment;filename=1.jpg");
    //设置响应状态码
    HttpStatus statusCode = HttpStatus.OK;
    //创建ResponseEntity对象
    ResponseEntity<byte[]> responseEntity = new ResponseEntity<>(bytes, headers, statusCode);
    //关闭输入流
    is.close();
    return responseEntity;
}

文件上传

文件上传要求form表单的请求方式必须为post,并且添加属性enctype="multipart/form-data"

SpringMVC中将上传的文件封装到MultipartFile对象中,通过此对象可以获取文件相关信息。

1、添加依赖:

xml 复制代码
<dependency>
  <groupId>commons-fileupload</groupId>
  <artifactId>commons-fileupload</artifactId>
  <version>1.3.1</version>
</dependency>

2、在SpringMVC的配置文件中添加配置(注意ID命名一定是 multipartResolver):

xml 复制代码
<!--必须通过文件解析器的解析才能将文件转换为MultipartFile对象-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <property name="defaultEncoding" value="utf-8"></property>
    <property name="maxUploadSize" value="2097152"></property><!--限制文件上传2M内  -->
    <property name="maxInMemorySize" value="40960"></property>
</bean>

3、编写控制层代码:

java 复制代码
@RequestMapping("/testUp")
public String testUp(MultipartFile photo, HttpSession session) throws IOException {
    //获取上传的文件的文件名
    String fileName = photo.getOriginalFilename();
    //处理文件重名问题
    String hzName = fileName.substring(fileName.lastIndexOf("."));
    fileName = UUID.randomUUID().toString() + hzName;
    //获取服务器中photo目录的路径
    ServletContext servletContext = session.getServletContext();
    String photoPath = servletContext.getRealPath("photo");
    File file = new File(photoPath);
    if(!file.exists()){
        file.mkdir();
    }
    String finalPath = photoPath + File.separator + fileName;
    //实现上传功能
    photo.transferTo(new File(finalPath));
    return "success";
}

拦截器的配置

SpringMVC 中的拦截器用于拦截控制器方法的执行。

SpringMVC 中的拦截器需要实现 HandlerInterceptor,必须在SpringMVC的配置文件中进行配置:

java 复制代码
<bean class="com.atguigu.interceptor.FirstInterceptor"></bean>
<ref bean="firstInterceptor"></ref>
<!-- 以上两种配置方式都是对DispatcherServlet所处理的所有的请求进行拦截 -->
    
<mvc:interceptor>
    <mvc:mapping path="/**"/>
    <mvc:exclude-mapping path="/testRequestEntity"/>
    <ref bean="firstInterceptor"></ref>
</mvc:interceptor>
<!-- 
	以上配置方式可以通过ref或bean标签设置拦截器,通过mvc:mapping设置需要拦截的请求,通过mvc:exclude-mapping设置需要排除的请求,即不需要拦截的请求
-->

<!-- Demo项目使用自定义Spring拦截器 -->
<mvc:interceptors>
    <mvc:interceptor>
        <mvc:mapping path="/**"/>
        <bean class="com.demo.common.interceptor.SpringMVCInterceptor"></bean>
    </mvc:interceptor>
</mvc:interceptors>

拦截器的三个抽象方法

SpringMVC中的拦截器有三个抽象方法:

1、preHandle:控制器方法执行之前执行preHandle(),其boolean类型的返回值表示是否拦截或放行,返回true为放行,即调用控制器方法;返回false表示拦截,即不调用控制器方法

2、postHandle:控制器方法执行之后执行postHandle()

3、afterComplation:处理完视图和模型数据,渲染视图完毕之后执行afterComplation()

拦截器的执行顺序

浏览器 - 过滤器 - DispatcherServlet - preHandle - 控制层 - postHandle - 视图渲染 - afterComplation

多个拦截器的执行顺序

a>若每个拦截器的preHandle()都返回true

此时多个拦截器的执行顺序和拦截器在SpringMVC的配置文件的配置顺序有关:

preHandle()会按照配置的顺序执行,而postHandle()和afterComplation()会按照配置的反序执行

b>若某个拦截器的preHandle()返回了false

preHandle()返回false和它之前的拦截器的preHandle()都会执行,postHandle()都不执行,返回false的拦截器之前的拦截器的afterComplation()会执行

拦截器源码分析

参考:DispatcherServlet#doDispatch

部分代码如下,关于执行顺序也从源码里面可以看出来,执行流程其实围绕着

处理执行链 HandlerExecutionChain 进行。

java 复制代码
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
	return;
}

// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
java 复制代码
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
	HandlerInterceptor[] interceptors = getInterceptors();
	if (!ObjectUtils.isEmpty(interceptors)) {
		for (int i = 0; i < interceptors.length; i++) {
			HandlerInterceptor interceptor = interceptors[i];
			if (!interceptor.preHandle(request, response, this.handler)) {
				triggerAfterCompletion(request, response, null);
				return false;
			}
			this.interceptorIndex = i;
		}
	}
	return true;
}

具体示例:

java 复制代码
//Step1. 自定义的拦截器,实现的接口HandlerInterceptor
public class CustomInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //预处理,返回true则继续执行。如果需要登录校验,校验不通过返回false即可,通过则返回true。
        System.out.println("执行preHandle()方法");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        //后处理
        System.out.println("执行postHandle()方法");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //在DispatcherServlet完全处理完请求后被调用
        System.out.println("执行afterCompletion()方法");
    }
}

/**
 * Step2. 注册拦截器到容器中,/**代表所有路径
 * @param registry 拦截器注册表对象
 */
@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new CustomInterceptor()).addPathPatterns("/**");
}

总结陈词

此篇文章介绍了SpringMVC 项目的基础搭建和一些知识介绍,仅供参考。

后续还会继续从 SpringMVC 的常见用法、源码分析、扩展点分析、企业实战等方面展开。

💗 后续会逐步分享企业实际开发中的实战经验,有需要交流的可以联系博主。

相关推荐
魔道不误砍柴功1 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2341 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨1 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
种树人202408191 小时前
如何在 Spring Boot 中启用定时任务
spring boot
Chrikk3 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*3 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue3 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man3 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
测开小菜鸟3 小时前
使用python向钉钉群聊发送消息
java·python·钉钉
P.H. Infinity4 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq