SpringMVC的高级特性

拦截器、文件上传和异常处理


拦截器

拦截器是 Spring MVC 框架提供的一种强大机制,允许你在请求处理的生命周期中 的特定点注入自定义逻辑。它类似于 Servlet 规范中的 Filter,但更紧密地与 Spring MVC 的上下文集成,可以直接访问 Spring 管理的 Bean 和 Handler 信息。

拦截器是 Spring MVC 在请求到达控制器前后以及视图渲染完成后,按配置顺序执行的可插拔处理器。

拦截器的作用与核心思想

作用

把「登录检查、日志、计时」这类到处都要写的代码,统一塞进 3 个固定时机里运行,让控制器只关心业务。(解耦)

3 个固定时机

preHandle :控制器方法运行前(可做权限拦截)

postHandle :控制器方法运行后,视图渲染前(可补充公共数据)

afterCompletion:整个请求完成后(可记录耗时、清理资源)

拦截链

多个拦截器按注册顺序串成一条链时,排队执行,其顺序为:

请求 → 1.pre → 2.pre → ... → Handler → ... → 2.post → 1.post → ... → 1.after → 2.after

核心接口HandlerInterceptor

实现自定义拦截器需要实现HandlerInterceptor 接口。该接口定义了三个关键方法,代表3个固定时机,上面已经提到过:

preHandle

java 复制代码
//方法签名
boolean preHandle(HttpServletRequest request,
                  HttpServletResponse response,
                  Object handler) throws Exception

调用时机: 在目标 Handler(控制器方法)执行之前被调用。

返回值:

  • true:继续执行拦截器链中的下一个拦截器,最终会执行目标 Handler。

  • false:中断执行流程,后续的拦截器和目标 Handler 都不会执行。

用途: 权限检查、登录验证、请求预处理(如设置请求属性)。

postHandle

java 复制代码
//方法签名
void postHandle(HttpServletRequest request,
                HttpServletResponse response,
                Object handler,
                ModelAndView modelAndView) throws Exception

调用时机: 在目标 Handler 执行完毕之后 ,但在视图渲染之前被调用。

参数 modelAndView 可以访问和修改控制器方法返回的 ModelAndView 对象

返回值:void

用途: 对模型数据进行后处理(如添加公共模型属性),修改视图信息。

afterCompletion

java 复制代码
//方法签名
void afterCompletion(HttpServletRequest request,
                     HttpServletResponse response,
                     Object handler,
                     Exception ex) throws Exception

调用时机:整个请求处理完成之后被调用,即视图渲染完成之后(无论是否发生异常)。

参数 ex: 如果请求处理过程中抛出了异常,此参数包含该异常对象;否则为 null

用途: 整个处理过程中抛出的异常(无异常时为 null),用于记录或清理资源(如关闭数据库连接)

拦截器与过滤器的对比

特性 拦截器 (Interceptor) 过滤器 (Filter)
作用范围 只拦截 Spring MVC 控制器请求 拦截所有请求(包括静态资源)
执行位置 控制器方法前后和视图渲染后 Servlet 的 service() 方法前后
依赖 Spring ✅ 是(Spring Bean) ❌ 否(Servlet 规范)
典型用途 1. 登录/权限检查 2. 日志记录 3. 添加全局模型数据 1. 统一编码设置 2. XSS/SQL 过滤 3. 压缩响应
获取控制器信息 ✅ 可获取控制器方法和参数 ❌ 不能
执行顺序 多个拦截器按注册顺序执行(pre顺序,post逆序) web.xml 配置顺序执行
  • 用过滤器(Filter) :处理所有请求的底层操作(安全过滤/编码转换)。

  • 用拦截器(Interceptor) :处理 Spring MVC 控制器相关的业务逻辑(权限/日志/数据处理)。

拦截器的配置方式

xml格式

适合传统项目或需要集中管理配置的场景

XML 复制代码
<!-- 配置拦截器 -->
<mvc:interceptors>
    <!-- 示例:登录验证拦截器 -->
    <mvc:interceptor>
        <!-- 拦截所有 /user 开头的请求 -->
        <mvc:mapping path="/user/**"/>
        <!-- 排除登录接口 -->
        <mvc:exclude-mapping path="/user/login"/>
        <!-- 声明拦截器 Bean -->
        <bean class="com.example.interceptor.AuthInterceptor"/>
    </mvc:interceptor>
    
    <!-- 可以继续添加其他拦截器 -->
</mvc:interceptors>

Java 配置类方式

推荐用于 Spring Boot 或基于注解配置的项目

java 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {

    // 注册拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AuthInterceptor())
                .addPathPatterns("/user/**")     // 拦截路径
                .excludePathPatterns("/user/login"); // 排除路径
        
        // 可以继续添加其他拦截器
        // registry.addInterceptor(new LogInterceptor()).addPathPatterns("/**");
    }
}

两种方式的实现类

java 复制代码
public class AuthInterceptor implements HandlerInterceptor {
    
    // 在控制器执行前调用
    @Override
    public boolean preHandle(HttpServletRequest request, 
                            HttpServletResponse response, 
                            Object handler) throws Exception {
        // 示例:检查用户是否登录
        if (request.getSession().getAttribute("user") == null) {
            response.sendRedirect("/login"); // 未登录重定向
            return false; // 中断请求
        }
        return true; // 继续执行
    }
    
    // 其他方法按需实现
    // postHandle(), afterCompletion()
}

文件上传的核心组件

MultipartResolver 是 Spring MVC 处理文件上传的入口接口,在Spring的主要实现为**StandardServletMultipartResolver**

  • 基于 Servlet 3.0+ 规范内置的文件上传功能。

  • 不需要额外依赖 (Servlet 3.0+ 容器如 Tomcat 7+, Jetty 9+ 已支持)。

  • 配置主要在 web.xml (Servlet 容器级别) 或通过 MultipartConfigElement 在 Servlet 注册时配置

web.xml 配置 (DispatcherServlet):

XML 复制代码
<servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <multipart-config>
        <max-file-size>5242880</max-file-size> <!-- 5MB -->
        <max-request-size>10485760</max-request-size> <!-- 10MB -->
        <file-size-threshold>0</file-size-threshold> <!-- 立即写入磁盘 -->
    </multipart-config>
</servlet>

Java Config (Spring Boot 或 Servlet 3.0+ 编程式配置):

java 复制代码
@Bean
public MultipartResolver multipartResolver() {
    return new StandardServletMultipartResolver();
}

// 配置 MultipartConfigElement (通常在 ServletRegistration.Dynamic 中设置)
@Bean
public ServletRegistrationBean<DispatcherServlet> dispatcherServlet() {
    ServletRegistrationBean<DispatcherServlet> registration = new ServletRegistrationBean<>(new DispatcherServlet(), "/");
    registration.setMultipartConfig(new MultipartConfigElement("", 5242880, 10485760, 0)); // 参数: location, maxFileSize, maxRequestSize, fileSizeThreshold
    return registration;
}

Spring Boot 自动配置: Spring Boot 会自动配置一个 StandardServletMultipartResolver。文件上传属性通过 application.properties/yml 配置:

java 复制代码
spring.servlet.multipart.max-file-size=5MB
spring.servlet.multipart.max-request-size=10MB
spring.servlet.multipart.location=/tmp/uploads # (可选) 临时存储目录
spring.servlet.multipart.file-size-threshold=0
spring.servlet.multipart.enabled=true # (默认true) 启用 multipart 支持

控制器处理上传文件

配置好MultipartResolver,就可以在控制器方法中使用特定的参数类型用来接收上传的文件和表单数据了。

MultipartFile 接口说明

java 复制代码
/**
 * MultipartFile 核心操作接口
 * -----------------------------------------
 * 使用场景示例:
 * 
 * // 1. 获取表单字段名(<input type="file" name="myFile"> 中的 "myFile")
 * String fieldName = file.getName();
 * 
 * // 2. 获取客户端原始文件名(包含路径信息,需注意安全处理)
 * String originalName = file.getOriginalFilename();
 * 
 * // 3. 获取文件MIME类型(如 "image/jpeg")
 * String mimeType = file.getContentType();
 * 
 * // 4. 检查空文件(未选择文件或0字节文件)
 * if(file.isEmpty()) {
 *     throw new RuntimeException("请选择有效文件");
 * }
 * 
 * // 5. 获取文件大小(字节数)
 * long size = file.getSize();
 * 
 * // 6. 获取字节数组(适合小文件内存操作)
 * byte[] bytes = file.getBytes();
 * 
 * // 7. 获取输入流(适合大文件流式处理)
 * InputStream is = file.getInputStream();
 * 
 * // 8.【最常用】直接保存到文件系统(自动处理临时文件清理)
 * File dest = new File("/safe/path/" + sanitizeFilename(file.getOriginalFilename()));
 * file.transferTo(dest); 
 * 
 * 安全提示:
 * - 使用 transferTo() 时避免直接使用原始文件名,防止路径遍历攻击
 * - 重要:需在配置中设置文件大小限制(防止DoS攻击)
 */
public interface MultipartFile {
    String getName();
    String getOriginalFilename();
    String getContentType();
    boolean isEmpty();
    long getSize();
    byte[] getBytes() throws IOException;
    InputStream getInputStream() throws IOException;
    void transferTo(File dest) throws IOException, IllegalStateException;
}

文件上传控制器

java 复制代码
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;

@Controller
public class FileUploadController {

    /**
     * 处理文件上传请求 - 使用@RequestParam注解接收文件和表单数据
     * 
     * @param file        单个文件上传(对应表单字段名"file")
     * @param description 普通文本字段(对应表单字段名"description")
     * @param files       多个文件(同名字段提交,数组形式接收)
     * @param fileList    多个文件(List集合形式接收,更灵活)
     * 
     * 处理流程:
     * 1. 接收并验证单个文件
     * 2. 处理多个文件数组
     * 3. 处理多个文件列表
     * 4. 处理普通表单字段
     */
    @PostMapping("/upload")
    public String handleFileUpload(
            // 接收单个文件(必须与表单<input type="file" name="file">的name属性匹配)
            @RequestParam("file") MultipartFile file,
            
            // 接收普通文本字段
            @RequestParam("description") String description,
            
            // 接收多个文件(适用于同名多个文件上传)
            @RequestParam("files") MultipartFile[] files,
            
            // 接收多个文件(List集合形式,更灵活的集合操作)
            @RequestParam("fileList") List<MultipartFile> fileList) throws IOException {
        
        // ==================== 处理单个文件 ====================
        if (!file.isEmpty()) {
            // 获取原始文件名(包含客户端路径信息)
            String fileName = file.getOriginalFilename();
            
            // 方法1:获取字节数组直接处理(适合小文件)
            byte[] bytes = file.getBytes();
            
            // 方法2【推荐】:直接转存到文件系统(自动处理临时文件清理)
            File dest = new File("/uploads/" + fileName);
            file.transferTo(dest);  // 核心保存方法
        }

        // ==================== 处理多个文件(数组形式) ====================
        for (MultipartFile f : files) {
            if (!f.isEmpty()) {
                // 获取表单字段名(<input type="file" name="files">中的name)
                String fieldName = f.getName();
                
                // 获取内容类型(如"image/png")
                String contentType = f.getContentType();
                
                // 保存文件(示例)
                f.transferTo(new File("/uploads/" + f.getOriginalFilename()));
            }
        }

        // ==================== 处理多个文件(List形式) ====================
        for (MultipartFile f : fileList) {
            if (!f.isEmpty()) {
                // 检查文件是否为空(未选择文件或0字节文件)
                boolean isFileEmpty = f.isEmpty();
                
                // 获取文件大小(字节数)
                long fileSize = f.getSize();
                
                // 使用输入流处理(适合大文件或流式处理)
                try (InputStream is = f.getInputStream()) {
                    // 流处理逻辑...
                }
            }
        }

        // ==================== 处理普通表单字段 ====================
        System.out.println("文件描述: " + description);

        return "redirect:/upload-success";
    }
}

提供了完整的文件上传流程及四种常见参数接收方式:

  1. 单个文件(MultipartFile)

  2. 文本字段(String)

  3. 多文件数组(MultipartFile[])

  4. 多文件集合(List<MultipartFile>)

上传配置类(添加到配置类)

  • setMaxUploadSize():总请求大小限制

  • setMaxUploadSizePerFile():单文件大小限制

  • setDefaultEncoding():解决中文乱码问题

java 复制代码
@Configuration
public class UploadConfig {

    /**
     * 配置文件上传解析器(必需)
     * 
     * 安全设置建议:
     * - 设置最大文件大小(防止DoS攻击)
     * - 设置最大请求大小(含多文件总和)
     * - 设置存储位置(避免使用系统临时目录)
     */
    @Bean
    public MultipartResolver multipartResolver() {
        CommonsMultipartResolver resolver = new CommonsMultipartResolver();
        resolver.setMaxUploadSize(10 * 1024 * 1024);       // 总文件大小不超过10MB
        resolver.setMaxUploadSizePerFile(5 * 1024 * 1024);  // 单个文件不超过5MB
        resolver.setDefaultEncoding("UTF-8");               // 解决文件名中文乱码
        return resolver;
    }
    
    // 文件名安全处理工具(防止路径遍历攻击)
    public static String sanitizeFilename(String filename) {
        return filename.replaceAll("[^a-zA-Z0-9\\.\\-]", "_").replaceAll("\\.\\.", "_");
    }
}

异常处理的原理

控制器抛出异常 => DispatcherServlet捕获 => 异常处理器链 => @ExceptionHandler、@ControllerAdvice、HandlerExceptionResolver => 生成响应

处理器链:按优先级尝试处理:

当前控制器的 @ExceptionHandler

全局 @ControllerAdvice

内置 HandlerExceptionResolver

异常处理的三层示例

配置层

java 复制代码
// 启用全局异常处理器
@ControllerAdvice // 核心注解:声明全局异常处理器
public class ExceptionConfig {
    
    /**
     * 配置生产环境响应策略
     * 原理:Spring Boot 自动读取此配置
     */
    @Bean
    public ErrorProperties errorProperties() {
        ErrorProperties props = new ErrorProperties();
        props.setIncludeException(false); // 生产环境隐藏异常信息
        props.setPath("/error");          // 统一错误处理路径
        return props;
    }
}

设计层

java 复制代码
// === 1. 异常分类设计(领域模型)===
/**
 * 业务异常基类(用户可见)
 * 最佳实践:继承RuntimeException避免声明式捕获
 */
public class BusinessException extends RuntimeException {
    private final String errorCode; // 业务错误码
    
    public BusinessException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }
    
    // 具体业务异常示例
    public static class UserNotFoundException extends BusinessException {
        public UserNotFoundException() {
            super("USER_NOT_FOUND", "用户不存在");
        }
    }
    
    public static class PaymentFailedException extends BusinessException {
        public PaymentFailedException(String detail) {
            super("PAYMENT_FAILED", "支付失败: " + detail);
        }
    }
}

// === 2. 控制器中的使用示例 ===
@RestController
@RequestMapping("/orders")
public class OrderController {
    
    @PostMapping("/{id}/pay")
    public void payOrder(@PathVariable Long id) {
        // 最佳实践1:业务校验抛出明确异常
        if (!orderService.exists(id)) {
            throw new BusinessException.OrderNotFoundException();
        }
        
        // 最佳实践2:服务层异常直接向上抛出
        paymentService.processPayment(id);
    }
    
    /**
     * 最佳实践3:参数校验异常自动处理
     * 原理:MethodArgumentNotValidException会被全局处理器捕获
     */
    @PostMapping
    public void createOrder(@Valid @RequestBody OrderCreateRequest request) {
        // 参数自动校验
    }
}

// === 3. 生产环境安全处理 ===
/**
 * 关键安全规则:
 * 1. 永远不返回堆栈信息给客户端
 * 2. 用户输入信息需过滤敏感内容
 * 3. 记录完整异常日志(含请求上下文)
 */
private String sanitizeMessage(String rawMessage) {
    // 过滤敏感信息(如身份证号、密码等)
    return rawMessage.replaceAll("(\d{6})\d{8}(\w{4})", "$1********$2");
}

实现层

java 复制代码
// === 1. 定义统一错误响应体 ===
@Data // Lombok简化
public class ErrorResult {
    private int status;     // HTTP状态码
    private String code;    // 业务错误码
    private String message; // 用户友好信息
    private String path;    // 请求路径
    private Instant time = Instant.now(); // 时间戳
}

// === 2. 全局异常处理器实现 ===
@ControllerAdvice
public class GlobalExceptionHandler {
    
    /**
     * 处理业务异常(自定义)
     * 实现原理:@ExceptionHandler 捕获特定异常类型
     */
    @ExceptionHandler(BusinessException.class)
    @ResponseBody
    public ResponseEntity<ErrorResult> handleBusinessEx(
            BusinessException ex, 
            HttpServletRequest request) {
        
        ErrorResult result = new ErrorResult();
        result.setStatus(400);
        result.setCode(ex.getErrorCode()); // 如: "USER_NOT_FOUND"
        result.setMessage(ex.getMessage());
        result.setPath(request.getRequestURI());
        
        return ResponseEntity.badRequest().body(result);
    }
    
    /**
     * 处理所有未捕获异常(兜底方案)
     */
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ResponseEntity<ErrorResult> handleUnknownEx(
            Exception ex, 
            HttpServletRequest request) {
        
        ErrorResult result = new ErrorResult();
        result.setStatus(500);
        result.setCode("SYSTEM_ERROR");
        result.setPath(request.getRequestURI());
        
        // 生产环境与开发环境差异化处理
        if (isProduction()) {
            result.setMessage("系统繁忙,请稍后再试"); // 生产环境友好提示
            log.error("系统异常: {} {}", request.getMethod(), request.getRequestURI(), ex);
        } else {
            result.setMessage(ex.getMessage()); // 开发环境显示详情
        }
        
        return ResponseEntity.internalServerError().body(result);
    }
    
    private boolean isProduction() {
        return Arrays.asList(env.getActiveProfiles()).contains("prod");
    }
}

异常处理三原则

明确性原则

java 复制代码
// 反例:模糊异常
throw new Exception("操作失败");

// 正例:明确业务异常
throw new BusinessException.PaymentFailedException("余额不足");

安全隔离原则

java 复制代码
// 生产环境响应
{
  "status": 500,
  "code": "SYSTEM_ERROR",
  "message": "服务不可用" // 非技术细节
}

// 开发环境响应
{
  "message": "NullPointer at UserService:38" // 技术细节
}

上下文完整原则

java 复制代码
log.error("请求失败 [{}] {}?{}", 
     request.getMethod(), 
     request.getRequestURI(),
     request.getQueryString(),
     ex); // 记录完整上下文

常用的异常状态码以及提示

状态码 名称 触发场景 用户提示
400 Bad Request 参数错误/缺失必填项 "请求参数错误,请检查后重试"
401 Unauthorized 未登录或Token过期 "登录已失效,请重新登录"
403 Forbidden 权限不足 "抱歉,您无权访问此内容"
404 Not Found 请求资源不存在 "您访问的内容不存在"
500 Internal Server Error 服务器内部错误 "系统繁忙,请稍后再试"