SpringMVC 控制层框架-下

五、SpringMVC其他扩展


1. 异常处理机制
1.1 异常处理概念

开发过程中是不可避免地会出现各种异常情况,例如网络连接异常、数据格式异常、空指针异常等等。异常的出现可能导致程序的运行出现问题,甚至直接导致程序崩溃。因此,在开发过程中,合理处理异常、避免异常产生、以及对异常进行有效的调试是非常重要的。

对于异常的处理,一般分为两种方式:

  • 编程式异常处理:是指在代码中显式地编写处理异常的逻辑。它通常涉及对异常类型的检测及其处理,例如使用 try-catch 块来捕获异常,然后在 catch 块中编写特定的处理代码,或者在 finally 块中执行一些清理操作。在编程式异常处理中,开发人员需要显式地进行异常处理,异常处理代码混杂在业务代码中,导致代码可读性较差。
  • 声明式异常处理:则是将异常处理的逻辑从具体的业务逻辑中分离出来,通过配置等方式进行统一的管理和处理。在声明式异常处理中,开发人员只需要为方法或类标注相应的注解(如 @Throws 或 @ExceptionHandler),就可以处理特定类型的异常。相较于编程式异常处理,声明式异常处理可以使代码更加简介、易于维护和扩展。

站在宏观角度来看待声明式事务处理:

整个项目从架构这个层面设计的异常处理的统一机制和规范。

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

使用声明式异常处理,可以统一项目处理异常思路,项目更加清晰了。

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

1)声明异常处理控制器类

异常处理控制类,统一定义异常处理 handler 方法

java 复制代码
/**
 * projectName: com.atguigu.execptionhandler
 * 
 * description: 全局异常处理器,内部可以定义异常处理Handler!
 */
/**
 * @RestControllerAdvice = @ControllerAdvice + @ResponseBody
 * @ControllerAdvice 代表当前类的异常处理controller! 
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

  
}

2)声明异常处理 handler 方法

异常处理 handler 方法和普通的 handler 方法参数接收和响应都一致。只不过异常处理 handler 方法要映射异常,发生对应的异常会调用。普通的 handler 方法要使用 @RequestMapping 注解映射路径,发生对应的路径调用。

java 复制代码
/**
 * 异常处理handler 
 * @ExceptionHandler(HttpMessageNotReadableException.class) 
 * 该注解标记异常处理Handler,并且指定发生异常调用该方法!
 * 
 * 
 * @param e 获取异常对象!
 * @return 返回handler处理结果!
 */
@ExceptionHandler(HttpMessageNotReadableException.class)
public Object handlerJsonDateException(HttpMessageNotReadableException e){
    
    return null;
}

/**
 * 当发生空指针异常会触发此方法!
 * @param e
 * @return
 */
@ExceptionHandler(NullPointerException.class)
public Object handlerNullException(NullPointerException e){

    return null;
}

/**
 * 所有异常都会触发此方法!但是如果有具体的异常处理Handler! 
 * 具体异常处理Handler优先级更高!
 * 例如: 发生NullPointerException异常!
 *       会触发handlerNullException方法,不会触发handlerException方法!
 * @param e
 * @return
 */
@ExceptionHandler(Exception.class)
public Object handlerException(Exception e){

    return null;
}

3)配置文件扫描控制器类配置

确保异常处理控制类被扫描

XML 复制代码
 <!-- 扫描controller对应的包,将handler加入到ioc-->
    <context:component-scan base-package="com.atguigu.controller,
    com.atguigu.exceptionhandler" />
2. 拦截器使用
2.1 拦截器概念

拦截器和过滤器解决问题

  • 生活中

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

  • 程序中

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

拦截器 VS 过滤器:

  • 相似点
    • 拦截:必须先把请求拦住,才能执行后续操作
    • 过滤:拦截器或过滤存在的意义就是对请求进行统一处理
    • 放行:对请求执行了必要操作后,放请求过去,让它访问原本想要访问的资源
  • 不同点
    • ​​​​​​​工作平台不同
      • ​​​​​​​过滤器工作在 Servlet 容器中
      • 拦截器工作在 SpringMVC 的基础上
    • 拦截的范围
      • ​​​​​​​过滤器:能够拦截到的最大范围是整个 Web 应用
      • 拦截器:能够拦截到的最大范围是整个 SpringMVC 负责的请求
    • IOC 容器支持
      • ​​​​​​​过滤器:想得到 IOC 容器需要调用专门的工具方法,是间接的
      • 拦截器:它自己就在 IOC 容器中,所以可以直接从 IOC 容器中装配组件,也就是可以直接得到 IOC 容器的支持

选择:

功能需要如果用 SpringMVC 的拦截器能够实现,就不使用过滤器。

2.2 拦截器使用

1)创建拦截器类

java 复制代码
public class Process01Interceptor implements HandlerInterceptor {

    // 在处理请求的目标 handler 方法前执行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("request = " + request + ", response = " + response + ", handler = " + handler);
        System.out.println("Process01Interceptor.preHandle");
         
        // 返回true:放行
        // 返回false:不放行
        return true;
    }
 
    // 在目标 handler 方法之后,handler报错不执行!
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("request = " + request + ", response = " + response + ", handler = " + handler + ", modelAndView = " + modelAndView);
        System.out.println("Process01Interceptor.postHandle");
    }
 
    // 渲染视图之后执行(最后),一定执行!
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("request = " + request + ", response = " + response + ", handler = " + handler + ", ex = " + ex);
        System.out.println("Process01Interceptor.afterCompletion");
    }
}

单个拦截器执行顺序:

  • preHandle() 方法
  • 目标 handler 方法
  • postHandle() 方法
  • 渲染视图(返回 json 没有此步骤)
  • afterCompletion() 方法

2)拦截器配置

springmvc.xml

XML 复制代码
<!-- 配置拦截器-->
<mvc:interceptors>
  <!-- 默认拦截器,拦截所有请求-->
  <bean class="com.atguigu.interceptor.Process01Interceptor" />
</mvc:interceptors>

3)配置详解

① 默认拦截全部

XML 复制代码
<!-- 具体配置拦截器可以指定拦截的请求地址 -->
<mvc:interceptor>
    <!-- 精确匹配 -->
    <mvc:mapping path="/common/request/one"/>
    <bean class="com.atguigu.mvc.interceptor.Process03Interceptor"/>
</mvc:interceptor>

② 精准配置

XML 复制代码
<!-- 具体配置拦截器可以指定拦截的请求地址 -->
<mvc:interceptor>
    <!-- 精确匹配 -->
    <mvc:mapping path="/common/request/one"/>
    <bean class="com.atguigu.mvc.interceptor.Process03Interceptor"/>
</mvc:interceptor>

<mvc:interceptor>
    <!-- /*匹配路径中的一层 -->
    <mvc:mapping path="/common/request/*"/>
    <bean class="com.atguigu.mvc.interceptor.Process04Interceptor"/>
</mvc:interceptor>

<mvc:interceptor>
    <!-- /**匹配路径中的多层 -->
    <mvc:mapping path="/common/request/**"/>
    <bean class="com.atguigu.mvc.interceptor.Process05Interceptor"/>
</mvc:interceptor>

③ 排除配置

XML 复制代码
<mvc:interceptor>
    <!-- /**匹配路径中的多层 -->
    <mvc:mapping path="/common/request/**"/>

    <!-- 使用 mvc:exclude-mapping 标签配置不拦截的地址 -->
    <mvc:exclude-mapping path="/common/request/two/bbb"/>

    <bean class="com.atguigu.mvc.interceptor.Process05Interceptor"/>
</mvc:interceptor>

④ 多个拦截器执行顺序

  1. preHandle() 方法:SpringMVC 会把所有拦截器收集到一起,然后按照配置顺序调用各个 preHandle() 方法。
  2. postHandle() 方法:SpringMVC 会把所有拦截器收集到一起,然后按照配置相反的顺序调用各个 postHandle() 方法。
  3. afterCompletion() 方法:SpringMVC 会把所有拦截器收集到一起,然后按照配置相反的顺序调用各个 afterCompletion() 方法。
2.3 拦截器作用位置图解
2.4 拦截器案例

一个网站有 5、6个资源,其中有一个为登录资源,两个无需登录即可访问,另外三个需要登录后才能访问,如果不登录就访问那三个资源,需要拦截,并且提示登录后访问。

访问资源的请求地址:

登录资源:/public/resource/login

公共资源1:/public/resource/one

公共资源2:/public/resource/two

私密资源1:/private/resource/one

私密资源2:/private/resource/two

私密资源3:/private/resource/three

案例实现:

1) 声明资源类

① PublicController

java 复制代码
/**
 * projectName: com.atguigu.controller
 * description: 公有资源控制类
 */
@RestController
@RequestMapping("public/resource")
public class PublicController {
    /**
     * 模拟登录,将假用户数据存储到session中!
     */
    @GetMapping("login")
    public Object login(HttpSession session){
        session.setAttribute("user","root");
        return "login success!!";
    }

    @GetMapping("one")
    public Object one(){
        return "public one";
    }

    @GetMapping("two")
    public Object two(){
        return "public two";
    }
}

② PrivateController

java 复制代码
@RestController
@RequestMapping("private/resource")
public class PrivateController {
    
    @GetMapping("one")
    public Object one(){
        return "private one";
    }
    
    @GetMapping("two")
    public Object two(){
        return "private two";
    }

    @GetMapping("three")
    public Object three(){
        return "private two";
    }

}

2)声明拦截器类

java 复制代码
/**
 * projectName: com.atguigu.interceptor
 *
 * description: 登录保护拦截器
 */
public class LoginProtectInterceptor implements HandlerInterceptor {

    /**
     * 登录保护方法
     * @param request current HTTP request
     * @param response current HTTP response
     * @param handler chosen handler to execute, for type and/or instance evaluation
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        Object user = request.getSession().getAttribute("user");

        if (user == null){
            response.setContentType("text/html;charset=utf-8");
            //没有登录
            response.getWriter().print("请先登录,再访问! <a href='/public/resource/login'>点击此处登录</a>");
            //拦截,不到达目标地址
            return false;
        }
        return true;
    }
}

3)配置拦截器类

XML 复制代码
<!-- 配置拦截器-->
<mvc:interceptors>
   <mvc:interceptor>
       <mvc:mapping path="/private/**"/>
       <bean class="com.atguigu.interceptor.LoginProtectInterceptor" />
   </mvc:interceptor>
</mvc:interceptors>
3. 参数校验

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

1)校验概述

JSR 303 是 Java 为Bean 数据合法性校验提供的标准框架,它已经包含在 JavaEE 6.0标准中。JSR 303 通过在 Bean 属性上标注类似于 @NoNull、@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)操作演示

导入依赖

XML 复制代码
<!-- 校验注解 -->
<dependency>
    <groupId>jakarta.platform</groupId>
    <artifactId>jakarta.jakartaee-web-api</artifactId>
    <version>9.1.0</version>
    <scope>provided</scope>
</dependency>
        
<!-- 校验注解实现-->        
<!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator -->
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>8.0.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>8.0.0.Final</version>
</dependency>

应用校验注解

java 复制代码
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Min;
import org.hibernate.validator.constraints.Length;

/**
 * projectName: com.atguigu.pojo
 */
public class User {
    //age   1 <=  age < = 150
    @Min(10)
    private int age;

    //name 3 <= name.length <= 6
    @Length(min = 3,max = 10)
    private String name;

    //email 邮箱格式
    @Email
    private String email;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

handler 标记和绑定错误收集

java 复制代码
@RestController
@RequestMapping("user")
public class UserController {

    /**
     * @Validated 代表应用校验注解! 必须添加!
     */
    @PostMapping("save")
    public Object save(@Validated @RequestBody User user,
                       //在实体类参数和 BindingResult 之间不能有任何其他参数, BindingResult可以接受错误信息,避免信息抛出!
                       BindingResult result){
       //判断是否有信息绑定错误! 有可以自行处理!
        if (result.hasErrors()){
            System.out.println("错误");
            String errorMsg = result.getFieldError().toString();
            return errorMsg;
        }
        //没有,正常处理业务即可
        System.out.println("正常");
        return user;
    }
}

3)易混总结

@NotNull (包装类型不为null)、@NotEmpty(集合类型长度大于0) 、@NotBlank(字符串,不为null,切不为" "字符串) 都是用于在数据校验中检查字段值是否为空的注解,但是它们的用法和校验规则有所不同。

4. 文件上传和下载
4.1 文件上传

1)文件上传表单页面

位置:index.xml

  • 第一点:请求方式必须是 POST
  • 第二点:请求体的编码方式必须是 multipart / form-data(通过form 标签的 enctype 属性设置)
  • 第三点:使用 input 标签、type 属性设置为 file 来生成文件上传框
html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form action="/save/picture" method="post" enctype="multipart/form-data">
      昵称:<input type="text" name="nickName" value="龙猫" /><br/>
      头像:<input type="file" name="headPicture" /><br/>
      背景:<input type="file" name="backgroundPicture" /><br/>
      <button type="submit">保存</button>
    </form>
</body>
</html>

2)springmvc 环境要求

pom.xml 添加依赖

XML 复制代码
<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.3.1</version>
</dependency>

配置文件上传处理器(springmvc配置)

XML 复制代码
<!-- 文件上传处理器,可处理 multipart/* 请求并将其转换为 MultipartFile 对象-->
<bean id="multipartResolver"
      class="org.springframework.web.multipart.support.StandardServletMultipartResolver">
</bean>

注:CommonsMultipartResolver 的 bean 的 id,必须是:multipartResolver 如果不是这个值,会在上传文件时报错 在 web.xml 文件中添加 Multipart 配置

XML 复制代码
<servlet>
    <servlet-name>yourAppServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <multipart-config>
        <!-- 定义文件上传时所需的最大值,单位为字节 -->
        <max-file-size>10485760</max-file-size>
        <!-- 定义单个上传文件的最大值,单位为字节 -->
        <max-request-size>20971520</max-request-size>
        <!-- 定义内存中存储文件的最大值,超过此大小的文件会写入到硬盘中 -->
        <file-size-threshold>5242880</file-size-threshold>
    </multipart-config>
    <load-on-startup>1</load-on-startup>
</servlet>

3)handler 方法接收数据

java 复制代码
/**
 * 上传的文件使用 MultipartFile 类型接收其相关数据
 * @param nickName
 * @param picture
 * @param backgroundPicture
 * @return
 * @throws IOException
 */
@PostMapping ("picture")
public String upload(String nickName, @RequestParam("headPicture") MultipartFile picture, @RequestParam("backgroundPicture")MultipartFile backgroundPicture) throws IOException {
    System.out.println(nickName);
 String inputName = picture.getName();
    System.out.println("文件上传表单项的 name 属性值:" + inputName);

    // 获取这个数据通常都是为了获取文件本身的扩展名
    String originalFilename = picture.getOriginalFilename();
    System.out.println("文件在用户本地原始的文件名:" + originalFilename);

    String contentType = picture.getContentType();
    System.out.println("文件的内容类型:" + contentType);

    boolean empty = picture.isEmpty();
    System.out.println("文件是否为空:" + empty);

    long size = picture.getSize();
    System.out.println("文件大小:" + size);

    byte[] bytes = picture.getBytes();
    System.out.println("文件二进制数据的字节数组:" + Arrays.asList(bytes));

    InputStream inputStream = picture.getInputStream();
    System.out.println("读取文件数据的输入流对象:" + inputStream);

    Resource resource = picture.getResource();
    System.out.println("代表当前 MultiPartFile 对象的资源对象" + resource);
    return "home";
}
```

4)MultipartFile 接口

5)文件转存

① 底层机制

② 本地转存

相关推荐
菠萝加点糖7 分钟前
Maven 坐标系统说明
java·maven
hm宋13 分钟前
Canal 升级 jar 包操作文档(以升级 FastJSON 为例)
java·canal·jar
后台模板学习20 分钟前
学习的心态高频面试题
java·数据库·学习
用户31268748772044 分钟前
线程池到底怎么管理线程的?从 ThreadPoolExecutor 到拒绝策略全链路拆解
java
cpolar技术支持1 小时前
Kafka Streams 窗口统计怎么验收:本地跑订单流聚合,用 cpolar 给同事看只读结果页
java·docker·kafka·cpolar·kafka streams
爱读源码的大都督1 小时前
DeepSeek面试官问:生产RAG系统回答不准确,该如何定位和优化?这样回答,能让面试官当场给你Offer!
java·后端·python
爱敲代码的小杨.1 小时前
【Spring】Spring Web MVC
前端·spring·mvc
杨运交2 小时前
[069][公共模块]Spring Boot 全局异常处理与参数校验实战(下):校验异常精细化处理与 WebFlux 适配
java·spring boot·后端
Raas1002 小时前
MAI Gateway(魔芋企业级AI网关)对比分析:AI网关和OpenRouter区别?企业级能力差距一览
java·服务器·网络·人工智能·gateway·ai网关·mai gateway
APItesterCris2 小时前
告别人工盯品!借助 Open‑Claw 快速搭建电商商品全自动监控与数据分析系统(完整实操代码)
java·大数据·前端·数据库