【SpringMVC-03】拦截器+类型转换器+数据校验以及异常映射,文件上传和下载

时间:2021-02-05 01:06:38

第一章 拦截器(重点)

1. 概念

1.1 使用场景

1.1.1 生活中坐地铁的场景

为了提高乘车效率,在乘客进入站台前统一检票:

【SpringMVC-03】拦截器+类型转换器+数据校验以及异常映射,文件上传和下载

1.1.2 程序中的校验登录场景

在程序中,使用拦截器在请求到达具体 handler 方法前,统一执行检测。

【SpringMVC-03】拦截器+类型转换器+数据校验以及异常映射,文件上传和下载

1.2 拦截器与过滤器的对比

1.2.1 相同点

三要素相同

  • 拦截(配置拦截路径):必须先把请求拦住,才能执行后续操作
  • 过滤(根据某种规则/业务逻辑进行筛选):拦截器或过滤器存在的意义就是对请求进行统一处理
  • 放行(满足规则/筛选条件,就让你访问你想访问的资源):对请求执行了必要操作后,放请求过去,让它访问原本想要访问的资源
1.2.2 不同点
  • 工作平台不同
  • 过滤器工作在 Servlet 容器中
  • 拦截器工作在 SpringMVC 的基础上
  • 拦截的范围
  • 过滤器:能够拦截到的最大范围是整个 Web 应用
  • 拦截器:能够拦截到的最大范围是整个 SpringMVC 负责的请求(handler方法、view-controller跳转页面、default-servlet-handler处理的静态资源)
  • IOC 容器支持
  • 过滤器:想得到 IOC 容器需要调用专门的工具方法,是间接的
  • 拦截器:它自己就在 IOC 容器中,所以可以直接从 IOC 容器中装配组件,也就是可以直接得到 IOC 容器的支持

2. 具体使用

2.1 创建拦截器类

public class Process01Interceptor implements HandlerInterceptor {
 
    Logger logger = LoggerFactory.getLogger(this.getClass());
 
    // 在处理请求的目标 handler 方法前执行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        
        logger.debug("Process01Interceptor preHandle方法");
         
        // 返回true:放行
        // 返回false:不放行
        return true;
    }
 
    // 在目标 handler 方法之后,渲染视图之前
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
 
        logger.debug("Process01Interceptor postHandle方法");
        
    }
 
    // 渲染视图之后执行
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        
        logger.debug("Process01Interceptor afterCompletion方法");
        
    }
}

单个拦截器执行顺序

  • preHandle() 方法
  • 目标 handler 方法
  • postHandle() 方法
  • 渲染视图
  • afterCompletion() 方法

2.2 注册拦截器

2.2.1 默认拦截全部请求
<!-- 注册拦截器 -->
<mvc:interceptors> 
    <!-- 直接通过内部 bean 配置的拦截器默认拦截全部请求(SpringMVC 范围内) -->
    <bean class="com.akiba.interceptor.Demo01Interceptor"/>
</mvc:interceptors>
2.2.2 配置拦截路径
2.2.2.1 精确匹配
<!-- 具体配置拦截器可以指定拦截的请求地址 -->
<mvc:interceptor>
    <!-- 精确匹配 -->
    <mvc:mapping path="/hello/sayHello"/>
    <bean class="com.akiba.interceptor.Demo01Interceptor"/>
</mvc:interceptor>

2.2.2.2 模糊匹配:匹配单层路径

<mvc:interceptor>
    <!-- /*匹配路径中的一层 -->
    <mvc:mapping path="/hello/*"/>
    <bean class="com.akiba.interceptor.Demo01Interceptor"/>
</mvc:interceptor>

2.2.2.3 模糊匹配:匹配多层路径

<mvc:interceptor>
    <!--模糊匹配多级目录-->
    <mvc:mapping path="/hello/**"/>

    <!--排除-->
    <mvc:exclude-mapping path="/hello/sayHello"/>

    <bean class="com.akiba.interceptor.Demo01Interceptor"/>
</mvc:interceptor>

2.3 多个拦截器执行顺序

  • preHandle()方法:和配置的顺序一样
  • 目标handler方法
  • postHandle()方法:和配置的顺序相反
  • 渲染视图
  • afterCompletion()方法:和配置的顺序相反

第二章 类型转换

SpringMVC 将『把请求参数注入到 POJO 对象』这个操作称为『数据绑定』,英文单词是 binding。数据类型的转换和格式化就发生在数据绑定的过程中。 类型转换和格式化是密不可分的两个过程,很多带格式的数据必须明确指定格式之后才可以进行类型转换。最典型的就是日期类型。

1. 自动类型转换

HTTP 协议是一个无类型的协议,我们在服务器端接收到请求参数等形式的数据时,本质上都是字符串类型。请看 javax.servlet.ServletRequest 接口中获取全部请求参数的方法:

public Map<String, String[]> getParameterMap();

而我们在实体类当中需要的类型是非常丰富的。对此,SpringMVC 对基本数据类型提供了自动的类型转换。例如:请求参数传入“100”字符串,我们实体类中需要的是 Integer 类型,那么 SpringMVC 会自动将字符串转换为 Integer 类型注入实体类。

2. 日期和数值类型转换

2.1 通过注解设定数据格式

package com.akiba.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.NumberFormat;

import java.util.Date;

/**
 * 包名:com.akiba.pojo
 *
 * @author Akiba
 * SpringMVC提供了一些注解,可以让我们进行一些手动类型转换
 * 1. DateTimeFormat注解:可以对日期时间类型进行转换
 * 2. NumberFormat注解:可以对数值类型进行转换
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date productDate;

    @NumberFormat(pattern = "###,###,###.###")
    private Double productPrice;
}

2.2 前端表单

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>首页</title>
    </head>
    <body>
        <form action="/springmvcday0303/hello/saveProduct" method="post">
            生产日期:<input type="text" name="productDate" value="1992-10-15 17:15:06" /><br/>
            产品价格:<input type="text" name="productPrice" value="111,222,333.444" /><br/>
            <button type="submit">保存</button>
        </form>
    </body>
</html>

2.3 handler 方法

@RequestMapping("/saveProduct")
public String saveProduct(Product product){
    logger.debug(product.toString());
    return "target";
}

2.4 数据绑定失败后处理方式

2.4.1 默认结果

【SpringMVC-03】拦截器+类型转换器+数据校验以及异常映射,文件上传和下载

2.4.2 BindingResult 接口

BindingResult 接口和它的父接口 Errors 中定义了很多和数据绑定相关的方法,如果在数据绑定过程中发生了错误,那么通过这个接口类型的对象就可以获取到相关错误信息。

【SpringMVC-03】拦截器+类型转换器+数据校验以及异常映射,文件上传和下载

2.4.3 重构 handler 方法
@RequestMapping("/saveProduct")
public String saveProduct(Product product, BindingResult bindingResult){
    if (bindingResult.hasErrors()) {
        //跳转到错误页面:显示错误数据
        return "error";
    }
    logger.debug(product.toString());
    return "target";
}
2.4.4 在页面上显示错误消息

页面是error.html,放在Thymeleaf前后缀控制范围之内

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
        <!-- 从请求域获取实体类信息时,属性名是按照类名首字母小写的规则 -->
        <!-- ${注入请求参数的实体类.出问题的字段} -->
        <p th:errors="${product.productDate}">这里显示具体错误信息</p>
        <p th:errors="${product.productPrice}">这里显示具体错误信息</p>
    </body>
</html>

3. 自定义类型转换器

3.1 创建实体类

3.1.1 Address
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Address {
    private String province;
    private String city;
    private String street;
}
3.1.2 Student
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date productDate;

    @NumberFormat(pattern = "###,###,###.###")
    private Double productPrice;

    private Address productAddress;
}

3.2 表单

现在我们希望通过一个文本框输入约定格式的字符串,然后转换为我们需要的类型,所以必须通过自定义类型转换器来实现,否则 SpringMVC 无法识别。

<form action="/springmvcday0303/hello/saveProduct" method="post">
    生产日期:<input type="text" name="productDate" value="1992-10-15 17:15:06" /><br/>
    产品价格:<input type="text" name="productPrice" value="111,222,333.444" /><br/>
    生产地: <input type="text" name="productAddress" value="福建省,厦门市,湖里区禾山街道"/>
    <button type="submit">保存</button>
</form>

3.3 handler 方法

@RequestMapping("/saveProduct")
public String saveProduct(Product product, BindingResult bindingResult){
    if (bindingResult.hasErrors()) {
        //跳转到错误页面:显示错误数据
        return "error";
    }
    logger.debug(product.toString());
    return "target";
}

在目前代码的基础上,我们没有提供自定义类型转换器,所以处理请求时看到如下错误日志:

Field error in object 'student' on field 'address': rejected value [aaa,bbb,ccc]; codes [typeMismatch.student.address,typeMismatch.address,typeMismatch.com.akiba.mvc.entity.Address,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.address,address]; arguments []; default message [address]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'com.akiba.mvc.entity.Address' for property 'address'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'com.akiba.mvc.entity.Address' for property 'address': no matching editors or conversion strategy found]]]

页面返回 400。

3.4 创建自定义类型转换器类

实现接口:org.springframework.core.convert.converter.Converter<S,T>

泛型 S:源类型(本例中是 String 类型)

泛型 T:目标类型(本例中是 Address 类型)

package com.akiba.converter;

import com.akiba.pojo.Address;
import org.springframework.core.convert.converter.Converter;

/**
 * 包名:com.akiba.converter
 *
 * @author Akiba
 * 编写自定义类型转换器:
 * 1. 写一个类实现Converter接口
 * 2. 重写convert方法进行转换
 * 3. 在springmvc的配置文件中配置类型转换器
 */
public class AddressConverter implements Converter<String , Address> {
    @Override
    public Address convert(String source) {
        //source就是要进行转换的那个字符串
        //1. 解析字符串:获取省、市、街道
        String[] strs = source.split(",");
        //2. 创建一个Address对象
        Address address = new Address();
        //3. 将省、市、街道设置到Address对象中
        address.setProvince(strs[0]);
        address.setCity(strs[1]);
        address.setStreet(strs[2]);
        return address;
    }

    @Override
    public <U> Converter<String, U> andThen(Converter<? super Address, ? extends U> after) {
        return null;
    }
}

3.5 在springmvc配置文件中注册类型转换器

<!-- 在 mvc:annotation-driven 中注册 FormattingConversionServiceFactoryBean -->
<mvc:annotation-driven conversion-service="formattingConversionService"/>

<!-- 在 FormattingConversionServiceFactoryBean 中注册自定义类型转换器 -->
<bean 
      class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <!-- 在 converters 属性中指定自定义类型转换器 -->
    <property name="converters">
        <set>
            <bean class="com.akiba.converter.AddressConverter"/>
        </set>
    </property>
</bean>

第三章 数据校验(重要)

在 Web 应用三层架构体系中,表述层负责接收浏览器提交的数据,业务逻辑层负责数据的处理。为了能够让业务逻辑层基于正确的数据进行处理,我们需要在表述层对数据进行检查,将错误的数据隔绝在业务逻辑层之外。

1. 数据校验概述

JSR 303 是 Java 为 Bean 数据合法性校验提供的标准,它已经包含在 JavaEE 6.0 标准中。JSR 303 通过在 Bean 属性上标注类似于 @NotNull、@Max 等标准的注解指定校验规则,并通过标准的验证接口对Bean进行验证。

注解

规则

@Null

标注值必须为 null

@NotNull

标注值不可为 null,但是可以为空字符串

@AssertTrue

标注值必须为 true

@AssertFalse

标注值必须为 false

@Min(value)

标注值必须大于或等于 value

@Max(value)

标注值必须小于或等于 value

@DecimalMin(value)

标注值必须大于或等于 value

@DecimalMax(value)

标注值必须小于或等于 value

@Size(max,min)

标注值大小必须在 max 和 min 限定的范围内

@Digits(integer,fratction)

标注值值必须是一个数字,且必须在可接受的范围内

@Past

标注值只能用于日期型,且必须是过去的日期

@Future

标注值只能用于日期型,且必须是将来的日期

@Pattern(value)

标注值必须符合指定的正则表达式

JSR 303 只是一套标准,需要提供其实现才可以使用。Hibernate Validator 是 JSR 303 的一个参考实现,除支持所有标准的校验注解外,它还支持以下的扩展注解:

注解

规则

@Email

标注值必须是格式正确的 Email 地址

@Length

标注值字符串大小必须在指定的范围内

@NotEmpty

标注值字符串不能是空字符串

@Range

标注值必须在指定的范围内

Spring 4.0 版本已经拥有自己独立的数据校验框架,同时支持 JSR 303 标准的校验框架。Spring 在进行数据绑定时,可同时调用校验框架完成数据校验工作。在SpringMVC 中,可直接通过注解驱动 mvc:annotation-driven 的方式进行数据校验。Spring 的 LocalValidatorFactoryBean 既实现了 Spring 的 Validator 接口,也实现了 JSR 303 的 Validator 接口。只要在Spring容器中定义了一个LocalValidatorFactoryBean,即可将其注入到需要数据校验的 Bean中。Spring本身并没有提供JSR 303的实现,所以必须将JSR 303的实现者的jar包放到类路径下。

配置 mvc:annotation-driven 后,SpringMVC 会默认装配好一个 LocalValidatorFactoryBean,通过在处理方法的入参上标注 @Validated 注解即可让 SpringMVC 在完成数据绑定后执行数据校验的工作。

2. 具体操作

前提:1. springmvc环境 2. Tomcat8及以上版本

2.1 引入依赖

<!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator -->
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.2.0.Final</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator-annotation-processor -->
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator-annotation-processor</artifactId>
    <version>6.2.0.Final</version>
</dependency>

2.2 应用校验规则

2.2.1 给要进行校验的字段添加上校验规则注解
@Data
@AllArgsConstructor
@NoArgsConstructor
public class President {
    // 字符串长度:[3,6]
    @Size(min = 3, max = 6)
    private String username;
    // 字符串必须满足Email格式
    @Email
    private String email;

}
2.2.2 给handler方法的形参加上Validated
@RequestMapping("/savePresident")
public String savePresident(@Validated President president){
    logger.debug(president.toString());
    return "target";
}

2.3 校验失败效果

日志:

Field error in object 'president' on field 'email': rejected value [aa]; codes [Email.president.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [president.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@4a6addb7,.*]; default message [不是一个合法的电子邮件地址] Field error in object 'president' on field 'email': rejected value [aa]; codes [Size.president.email,Size.email,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [president.email,email]; arguments []; default message [email],6,3]; default message [个数必须在3和6之间]]]

同时页面返回 400。

2.4 显示友好的错误提示

2.4.1 重构 handler 方法
@RequestMapping("/save/president")
public String savePresident(
        // 在实体类参数和 BindingResult 之间不能有任何其他参数
        @Validated President president, BindingResult bindingResult) {
 
    if (bindingResult.hasErrors()) {
        return "error";
    }
     
    logger.debug(president.getEmail());
 
    return "target";
}
2.4.2 准备错误信息页面
<h1>系统信息</h1>
<!-- 从请求域获取实体类信息时,属性名是按照类名首字母小写的规则 -->
<!-- ${注入请求参数的实体类.出问题的字段} -->
<p th:errors="${president.email}">这里显示系统提示消息</p>

第四章 异常映射(了解)

1. 为什么需要异常映射

一个项目中会包含很多个模块,各个模块需要分工完成。如果张三负责的模块按照 A 方案处理异常,李四负责的模块按照 B 方法处理异常……各个模块处理异常的思路、代码、命名细节都不一样,那么就会让整个项目非常混乱。

异常映射可以将异常类型和某个具体的视图关联起来,建立映射关系。好处是可以通过 SpringMVC 框架来帮助我们管理异常。

  • 声明式管理异常:在配置文件中指定异常类型和视图之间的对应关系。在配置文件或注解类中统一管理。
  • 编程式管理异常:需要我们自己手动 try ... catch ... 捕获异常,然后再手动跳转到某个页面。

2. 异常映射的优势

  • 使用声明式代替编程式来实现异常管理
  • 让异常控制和核心业务解耦,二者各自维护,结构性更好
  • 整个项目层面使用同一套规则来管理异常
  • 整个项目代码风格更加统一、简洁
  • 便于团队成员之间的彼此协作

3. 基于 XML 的异常映射

3.1 XML配置

SpringMVC 会根据异常映射信息,在捕获到指定异常对象后,将异常对象存入请求域,然后转发到和异常类型关联的视图。

<!--配置异常处理-->
<bean 
      class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">

    <!-- 配置异常映射关系 -->
    <property name="exceptionMappings">
        <props>
            <!-- key属性:指定异常类型 -->
            <!-- 文本标签体:和异常类型对应的逻辑视图 -->
            <prop key="java.lang.ArithmeticException">error-arith</prop>

            <prop key="java.lang.ClassNotFoundException">error-class</prop>

            <prop key="java.lang.RuntimeException">error-runtime</prop>
        </props>
    </property>
</bean>

3.2 异常范围

如果在配置文件中,发现有多个匹配的异常类型,那么 SpringMVC 会采纳范围上最接近的异常映射关系。

<prop key="java.lang.ArithmeticException">error-arith</prop>
<prop key="java.lang.RuntimeException">error-runtime</prop>

4. 基于注解的异常映射

4.1 创建异常处理器类

【SpringMVC-03】拦截器+类型转换器+数据校验以及异常映射,文件上传和下载

4.2 异常处理器对象加入 IOC 容器

4.2.1 包扫描
<!--1.包扫描-->
<context:component-scan base-package="com.akiba"/>
4.2.2 给异常处理器类标记注解
// 异常处理器类需要使用 @ControllerAdvice 注解标记
@ControllerAdvice
public class MyExceptionHandler {
    
}

4.3 声明处理异常的方法

// @ExceptionHandler注解:标记异常处理方法
// value属性:指定匹配的异常类型
// 异常类型的形参:SpringMVC 捕获到的异常对象
@ExceptionHandler(value = NullPointerException.class)
public String resolveNullPointerException(Exception e, Model model) {
 
    // 我们可以自己手动将异常对象存入模型
    model.addAttribute("akiba", e);
 
    // 返回逻辑视图名称
    return "error-nullpointer";
}

当同一个异常类型在基于 XML 和注解的配置中都能够找到对应的映射,那么以注解为准。

5. 区分请求类型

5.1 为什么要区分请求类型

异常处理机制和拦截器机制都面临这样的问题:

【SpringMVC-03】拦截器+类型转换器+数据校验以及异常映射,文件上传和下载

5.2 判断依据

查看请求消息头中是否包含 Ajax 请求独有的特征:

  • Accept 请求消息头:包含 application/json
  • X-Requested-With 请求消息头:包含 XMLHttpRequest

两个条件满足一个即可。

/**
     * 判断请求类型,如果返回true就是同步请求,返回false就是异步请求
     * @param request
     * @return
     */
private boolean judgeRequestType(HttpServletRequest request) {
    //加入代码判断当前请求是同步请求还是异步请求:通过accept或者X-Requested-With
    String acceptHeader = request.getHeader("Accept");
    String xRequestHeader = request.getHeader("X-Requested-With");

    return !((acceptHeader != null && acceptHeader.contains("application/json")) ||
             (xRequestHeader != null && xRequestHeader.equals("XMLHttpRequest")));
}

5.3 重构异常处理器兼容两种请求的处理方法

@ExceptionHandler(value = Exception.class)
public String resolveNullPointerException(HttpServletResponse response,
                                          HttpServletRequest request,
                                          Exception e, Model model) throws IOException {
    //这个方法处理空指针异常
    if(!judgeRequestType(request)){
        //说明当前是异步请求:使用response向客户端响应异常信息
        response.getWriter().write(e.getMessage());
        return null;
    }
    //要获取异常信息,并且将异常信息存储到请求域
    model.addAttribute("akibaException",e);
    //这个方法返回:异常处理页面的逻辑视图
    return "error";
}

第五章 文件上传(重要)

1. 前端表单

需要满足的要求:

  • 第一点:请求方式必须是 POST
  • 第二点:请求体的编码方式必须是 multipart/form-data(通过 form 标签的 enctype 属性设置)
  • 第三点:使用 input 标签、type 属性设置为 file 来生成文件上传框
<form th:action="@{/akiba/upload}" method="post" enctype="multipart/form-data">
    
    <input type="file" name="picture" />
    <button type="submit">上传头像</button>
     
</form>

2. SpringMVC 环境

2.1 引入依赖

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

2.2 配置

在 SpringMVC 的配置文件中加入 multipart 类型数据的解析器:

<!--配置文件上传的解析器-->
<bean 
      class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <!-- 由于上传文件的表单请求体编码方式是 multipart/form-data 格式,所以要在解析器中指定字符集 -->
    <property name="defaultEncoding" value="UTF-8"/>
    <!--指定上传文件的最大体积: 8M-->
    <property name="maxUploadSize" value="8388608"/>
</bean>

3. handler 方法接收数据

@RequestMapping("/simple/upload")
public String doUpload(
 
        // 表单提交的数据仍然是请求参数,所以使用 @RequestParam 注解接收
        @RequestParam("nickName") String nickName,
 
        // 对于上传的文件使用 MultipartFile 类型接收其相关数据
        @RequestParam("picture") MultipartFile picture
        ) throws IOException {
 
    String inputName = picture.getName();
    logger.debug("文件上传表单项的 name 属性值:" + inputName);
 
    // 获取这个数据通常都是为了获取文件本身的扩展名
    String originalFilename = picture.getOriginalFilename();
    logger.debug("文件在用户本地原始的文件名:" + originalFilename);
 
    String contentType = picture.getContentType();
    logger.debug("文件的内容类型:" + contentType);
 
    boolean empty = picture.isEmpty();
    logger.debug("文件是否为空:" + empty);
 
    long size = picture.getSize();
    logger.debug("文件大小:" + size);
 
    byte[] bytes = picture.getBytes();
    logger.debug("文件二进制数据的字节数组:" + Arrays.asList(bytes));
 
    InputStream inputStream = picture.getInputStream();
    logger.debug("读取文件数据的输入流对象:" + inputStream);
 
    Resource resource = picture.getResource();
    logger.debug("代表当前 MultiPartFile 对象的资源对象" + resource);
 
    return "target";
}

4. MultipartFile接口介绍

【SpringMVC-03】拦截器+类型转换器+数据校验以及异常映射,文件上传和下载

5. 文件转存

5.1 底层原理

【SpringMVC-03】拦截器+类型转换器+数据校验以及异常映射,文件上传和下载

5.2 三种去向

5.2.1 本地转存

【SpringMVC-03】拦截器+类型转换器+数据校验以及异常映射,文件上传和下载

5.2.1.1 创建保存文件的目录

【SpringMVC-03】拦截器+类型转换器+数据校验以及异常映射,文件上传和下载

这个目录如果是空目录,那么服务器部署运行时很容易会忽略这个目录。为了避免这个问题,在这个目录下随便创建一个文件,随便写点内容即可。

5.2.1.2 编写转存代码

下面是负责处理文件上传请求的 handler 方法的转存部分:

@Autowired
private ServletContext servletContext;
Logger logger = LoggerFactory.getLogger(this.getClass());
@RequestMapping("/upload")
public String upload(@RequestParam("nickname") String nickname,@RequestParam("picture") MultipartFile multipartFile) throws IOException {
    //获取请求参数(除了文件之外),还是和以前一样的方式获取
    logger.debug(nickname);
    //multipartFile对象就表示客户端上传的文件
    //1. 指定转存的目录路径:动态获取部署的目录路径
    String dirPath = servletContext.getRealPath("head-picture");

    //2. 获取文件名:在获取到原文件的文件名之后,将其文件名改成一个唯一的名字,这样就能保证不会出现上传文件同名的情况
    String UUIDName = UUID.randomUUID().toString().replace("-","");
    //获取文件名的后缀
    String originalFilename = multipartFile.getOriginalFilename();
    String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));

    //唯一的文件名就是UUIDName拼接后缀
    String fileName = UUIDName + suffix;
    //要将该文件转存到目录中
    multipartFile.transferTo(new File(dirPath+"/"+fileName));
    return "target";
}
5.2.1.3 缺陷
  • Web 应用重新部署时通常都会清理旧的构建结果,此时用户以前上传的文件会被删除,导致数据丢失。
  • 项目运行很长时间后,会导致上传的文件积累非常多,体积非常大,从而拖慢 Tomcat 运行速度。
  • 当服务器以集群模式运行时,文件上传到集群中的某一个实例,其他实例中没有这个文件,就会造成数据不一致。
  • 不支持动态扩容,一旦系统增加了新的硬盘或新的服务器实例,那么上传、下载时使用的路径都需要跟着变化,导致 Java 代码需要重新编写、重新编译,进而导致整个项目重新部署。

【SpringMVC-03】拦截器+类型转换器+数据校验以及异常映射,文件上传和下载

5.2.2 文件服务器

【SpringMVC-03】拦截器+类型转换器+数据校验以及异常映射,文件上传和下载

5.2.2.1 优势
  • 不受 Web 应用重新部署影响
  • 在应用服务器集群环境下不会导致数据不一致
  • 针对文件读写进行专门的优化,性能有保障
  • 能够实现动态扩容

【SpringMVC-03】拦截器+类型转换器+数据校验以及异常映射,文件上传和下载

5.2.2.2 常见的文件服务器类型
  • 第三方平台:
  • 阿里的 OSS 对象存储服务
  • 七牛云
  • 自己搭建服务器:FastDFS等
5.2.3 上传到其他模块(了解)

这种情况肯定出现在分布式架构中,常规业务功能不会这么做,采用这个方案的一定的特殊情况。

【SpringMVC-03】拦截器+类型转换器+数据校验以及异常映射,文件上传和下载

第六章 文件下载(了解)

1. 原始形态

使用链接地址指向要下载的文件。此时浏览器会尽可能解析对应的文件,只要是能够在浏览器窗口展示的,就都会直接显示,而不是提示下载。

<a href="download/hello.akiba">下载</a><br/>
<a href="download/tank.jpg">下载</a><br/>
<a href="download/chapter04.zip">下载</a><br/>

上面例子中,只有 chapter04.zip 文件是直接提示下载的,其他两个都是直接显示。

2. 明确要求浏览器提示下载

@Autowired
private ServletContext servletContext;
@RequestMapping("/download")
public ResponseEntity download(@RequestParam("fileName") String fileName) throws IOException {
    //用来下载文件:
    //1. 使用输入流读取要下载的文件
    //1.1 获取要下载的文件路径
    String downloadPath = servletContext.getRealPath("download/"+fileName);
    //1.2 读取要下载的文件
    FileInputStream fileInputStream = new FileInputStream(downloadPath);
    int len = fileInputStream.available();
    byte[] buffer = new byte[len];
    fileInputStream.read(buffer);

    //2. 将要下载的文件输出到浏览器:使用响应
    //ResponseEntity表示响应实体:要下载的文件是通过响应体响应给客户端的
    //创建添加响应头
    MultiValueMap<String, String> headers = new HttpHeaders();
    //获取要下载的文件的mime-type,设置Content-Type响应头
    String contentType = servletContext.getMimeType(fileName);
    headers.add("Content-Type",contentType);

    //Content-Disposition:响应头是指示客户端下载内容
    headers.add("Content-Disposition","attachment;filename="+fileName);

    return new ResponseEntity<>(buffer, headers,HttpStatus.OK);
}

3. 典型应用场景举例

我们目前实现的是一个较为简单的下载,可以用在下面的一些场合:

  • 零星小文件下载
  • 将系统内部的数据导出为 Excel、PDF 等格式,然后以下载的方式返回给用户