06 SpringMVC之全局异常处理机制(TBD)+拦截器+参数校验+上传文件

1. 拦截器

1.1 什么是拦截器?

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

1.2 什么时候拦截器会生效?

调用handler之前(preHandler)和之后(postHandler),DS返回结果给前端之前(afterCompletion)

Springmvc中的拦截器 VS javaWeb中的过滤器:

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

总结 : 拦截器是对过滤器的进一步升级 , 能用拦截器就没必要用过滤器
在这里插入图片描述


1.3 拦截器使用

  1. 创建拦截器类 , 实现HandlerInterceptor:
package com.sunsplanter.interceptors

public class MyInterceptor implements HandlerInterceptor {

    // if( ! preHandler()){return;}
    //preHandle在处理请求的目标 handler 方法前执行
    //根据返回值true/false决定是否放行

/**
 *request 请求
 *response响应
 *handler 处理之前要判断是否拦截的handler方法
 *modelAndView 返回的视图和共享域数据对象
 */
    @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");
         
        return true;
    }
 
    // postHandle在目标 handler 方法后执行,若handler报错则该postHandler不执行!
    @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");
    }
 
    // afterCompletion在渲染视图之后执行(最后),一定执行!
    @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("MyInterceptor.afterCompletion");
    }
}
  1. 在config配置类中添加(注册)第一步中编写的各个拦截器
@EnableWebMvc  //json数据处理,必须使用此注解,因为他会加入json处理器
@Configuration
@ComponentScan(basePackages = {"com.sunsplanter.controller","com.sunsplanter.interceptors"}) 
//WebMvcConfigurer springMvc进行组件配置的规范,配置组件,提供各种方法! 
public class SpringMvcConfig implements WebMvcConfigurer {

    //配置jsp对应的视图解析器
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        //快速配置jsp模板语言对应的
        registry.jsp("/WEB-INF/views/",".jsp");
    }

    //开启静态资源处理 <mvc:default-servlet-handler/>
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

    //添加拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) { 
        //将MyInterceptor中的所有拦截器都添加到Springmvc环境,默认拦截所有Springmvc分发的请求
        registry.addInterceptor(new MyInterceptor());
    }
}
  1. 拦截器的进一步配置

a. 默认拦截全部(即上述)

@Override
public void addInterceptors(InterceptorRegistry registry) {
    //将拦截器添加到Springmvc环境,默认拦截所有Springmvc分发的请求
    registry.addInterceptor(new MyInterceptor());
}

b . 精准配置

@Override
public void addInterceptors(InterceptorRegistry registry) {
    //精准匹配,设置拦截器处理指定请求 路径可以设置一个或者多个,为项目下路径即可
    //addPathPatterns("/common/request/one") 添加拦截路径
    //也支持 /* 和 /** 模糊路径。 * 任意一层字符串 ** 任意层 任意字符串
    //拦截user下的所有handler
    registry.addInterceptor(new MyInterceptor()).addPathPatterns("/user/**");
}

c. 排除配置

//添加拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
    //排除匹配,排除已在匹配的范围的handler
    //addPathPatterns("/user/**") 添加拦截路径
    //excludePathPatterns("/user/data"); 排除路径,排除应该在拦截的范围内
    registry.addInterceptor(new Process01Interceptor())
            .addPathPatterns("/user/**")
            .excludePathPatterns("/user/data");
}

1.4 多个拦截器

1.4.1 多个拦截器的执行顺序

SpringMVC 会把所有拦截器收集到一起 , 然后:

a. 按照配置顺序调用各个 preHandle() 方法。
b. 按照配置逆序调用各个 postHandle() 方法。
c. 按照配置逆序调用各个 afterCompletion() 方法。

1.4.2 实例

需求: 验证登录用户的拦截器先执行,权限拦截器后执行,order()方法设置顺序,整数值越小,先执行(在拦截器具体实现中, 只使用了prehandler方法, 顺序执行)。

@Override
public void addInterceptors(InterceptorRegistry registry) {

	LoginInterceptor loginInterceptor = new LoginInterceptor();
		registry.addInterceptor(loginInterceptor)
			.order(1)
			.addPathPatterns("/**") //拦截所有请求
			.excludePathPatterns("/article/query"); //排除/article/query 请求
	
	AuthInterceptor authInterceptor= new AuthInterceptor();
		registry.addInterceptor(authInterceptor)
			.order(2)
			.addPathPatterns("/article/**") //拦截 article 开始的所有请求
			.excludePathPatterns("/article/query"); //排除/article/query 请求
}

1.5 拦截器和原生Filter的区别

Filters
作用:Filter 用于在 Servlet 处理请求之前或之后对请求/响应进行预处理/后处理。它们主要用于处理跨域请求、日志记录、请求响应修改、身份验证等。

拦截器:Spring 的 Interceptors 可以执行类似于 Filter 的任务,但仅限于 Spring 的 DispatcherServlet 处理的请求。如果需要在 Servlet 层级更早地处理请求(例如,静态资源请求),Filter 仍然是必要的。

2. 全局异常处理机制

2.1 两种异常处理方式

开发过程中是不可避免地会出现各种异常情况的,例如网络连接异常、数据格式异常、空指针异常等等。

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

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

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

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

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

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

2.2 基于注解的异常处理

处理流程:

发生异常 --> 去往ControllerAdvice(RestControllerAdvice)注解的类 --> 寻找@ExceptionHandler(绑定的类名.class)注解的异常处理类

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

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

/**
 * projectName: com.sunsplanter.execptionhandler
 * 
 * description: 全局异常处理器,内部可以定义异常处理Handler!
 * 
 * @RestControllerAdvice = @ControllerAdvice + @ResponseBody
 */
@RestControllerAdvice
public class GlobalExceptionHandler {
}
  1. 在异常处理控制器类中声明异常处理handler方法

异常处理handler方法和普通的handler方法参数接收和响应都一致!

只不过异常处理handler方法要映射异常,发生对应的异常会调用!

/**
 * 当发生空指针异常会触发此方法!
 * 这是具体异常处理Handler,如果所有具体异常处理handler都不匹配,就走@ExceptionHandler(Exception.class)
 */
@ExceptionHandler(NullPointerException.class)
public Object handlerNullException(NullPointerException e){

    return null;
}

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

    return null;
}

记得将异常处理控制类所在的包加入包扫描.

2.3 实例

需求: 应用计算两个数字相除,当用户被除数为 0 ,发生异常。使用自定义异常处理器代替默认的异常处理程序
在这里插入图片描述

2.3.1 前端代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="divide" method="get">&nbsp;&nbsp;&nbsp;数:<input name="n1" /> <br/>
    被除数:<input name="n2" /> <br/>
    <input type="submit" value="计算">
</form>
</body>
</html>

错误页面要动态取值, 才能展示, 所以放到templates下

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
错误原因:<h3 th:text="${error}"></h3>
</body>
</html>

2.3.2 后端代码之controller

package com.sunsplanter.exception1.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class NumberController {
    @GetMapping("/divide")
    public String some(Integer n1,Integer n2){
        int result = n1 / n2;
        return "n1/n2=" + result;
    }
}

2.3.3 后端代码之异常处理类

import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class GlobalExceptionHandler {
    //用视图页面作为展示
    @ExceptionHandler({ArithmeticException.class})
    public String handleArithmeticException(ArithmeticException e, Model model){
        String error = e.getMessage();
        model.addAttribute("error",error);
        return "error";
    }
    //不带视图,直接返回数据
/*
@ExceptionHandler({ArithmeticException.class})
@ResponseBody public Map<String,Object>
handleArithmeticExceptionReturnData(ArithmeticException e){
String error = e.getMessage();
Map<String,Object> map = new HashMap<>();
map.put("错误原因", e.getMessage());
map.put("解决方法", "输入的被除数要>0");
return map;
}*/
//其他异常
    @ExceptionHandler({Exception.class})
    @ResponseBody
    public Map<String,Object> handleRootException(Exception e){
        String error = e.getMessage();
        Map<String,Object> map = new HashMap<>();
        map.put("错误原因", e.getMessage());
        map.put("解决方法", "请稍候重试");
        return map;
    }
}

留待处理
先进入localhost:8080/input.html, 输入1/0后不能正确跳转error,

而是进入http://localhost:8080/divide?n1=1&n2=0

并且提示This application has no explicit mapping for /error, so you are seeing this as a fallback.

后台反馈Error resolving template [/error], template might not exist or might not be accessible by any of the configured Template Resolvers


3. 参数校验

什么是参数校验?

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

一切的开始是导入依赖:
使用参数校验的前提是同时开启@EnableWebMvc和添加依赖

<!-- 校验注解 -->
<dependency>
    <groupId>jakarta.platform</groupId>
    <artifactId>jakarta.jakartaee-web-api</artifactId>
    <version>9.1.0</version>
    <scope>provided</scope>
</dependency>
        
<!-- 校验注解实现-->        

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>8.0.0.Final</version>
</dependency>

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator-annotation-processor</artifactId>
    <version>8.0.0.Final</version>
</dependency>

后续在SB中,只要添加一个依赖即可:

	        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

步骤是 :

  1. 在实体类(Bean)属性中用用注解限定属性的取值范围
  2. 在handler中标记和绑定错误收集
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Min;
import org.hibernate.validator.constraints.Length;

/**
 * projectName: com.sunsplanter.pojo
 */
 @Data
public class User {
    
    @NotNull
    private int id;
    
    //age   1 <=  age < = 150
    @Min(1)
    private int age;

	//同一个属性可以指定多个注解,字符串用NotBlank校验
	@NotBlank(message = "文章必须有标题"}
    @Length(min = 3,max = 10)
    private String name;

    //email 邮箱格式
    @Email
    private String email;
}
@RestController
@RequestMapping("user")
public class UserController {

    /**
     * @Validated 代表应用校验注解! 必须添加!
     */
    @PostMapping("save")
    public Object Insert(@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;
    }
}

常用的校验注解就是非空和长度限定 , 其中存在三种非空 :

  1. @NotNull : 包装类型不为null
  2. @NotEmpty : 集合类型长度大于0 , 如Collection/Map/数组对象
  3. @NotBlank : 校验字符串,确保字符串不为null/或只包含空格

3.1 用全局异常实现参数校验(SpringBoot框架实现)

使用 JSR-303 (Hibernate实现)验证参数时,我们是在 Controller 方法,声明 BindingResult 对象获取校验结果。

但是Controller 的方法很多,每个方法都加入 BindingResult 处理检验参数比较繁琐。

现在可以校验参数失败抛出异常给框架,而异常处理器能够捕获到 MethodArgumentNotValidException,它是 BindException 的子类。

在这里插入图片描述

3.1.1 实例

需求: 全局处理JSR-303异常

3.1.1.1 添加依赖

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
3.1.1.2 创建Bean对象, 这一步不需要改变,完全采用上例
3.1.1.3 创建异常处理器
@RestControllerAdvice
public class GlobalExceptionHandler2 {

//校验参数异常
@ExceptionHandler({BindException.class})
//BindException会自动捕获所有异常, 只要从中提取即可
public Map<String,Object> handleJSR303Exception(BindException e){
	Map<String,Object> map = new HashMap<>();
	BindingResult result = e.getBindingResult();
	if (result.hasErrors()) {
		List<FieldError> errors = result.getFieldErrors();
		errors.forEach(field -> {
			map.put("错误["+field.getField()+"]原因",field.getDefaultMessage());
	});
}
// map会被框架转化成JSON返回
return map
3.1.1.4 测试
POST http://localhost:8080/order/new
Content-Type: application/json
{
"name": "每日订单",
"amount": 0,
"userId": 0
}

显示:
{
“错误[userId]原因”: “从 1 开始”,
“错误[amount]原因”: “一个订单商品数量在 1-99”
}

4. 分组参数校验

在上例中主键 id 是系统生成的, 在新增用户时不需要指定id。现在要修改/删除用户的某个属性,此时 id 必须有值,才能识别到去修改哪条数据库数据
也即新增操作时不对id有约束, 修改和删除时对id有约束, 因此, 不能仅仅写一个@NotNull

我们通过分组校验实现,.

给每个属性分配所需的不同规则,并声明该组名

@Data
public class User {
    
//新增组
public static interface AddArticleGroup { };
//编辑修改组
public static interface EditArticleGroup { };

	@NotNull(message = "用户 ID 不能为空", groups = { InsertUserGroup.class } )
	@Min(value = 1, message = "用户 ID 从 1 开始",groups = { InsertUserGroup.class } )
    private int id;
    
    //age   1 <=  age < = 150
    @Min(value = 1)
    private int age;

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

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

    public int getAge() {
        return age;
    }
}

在Conroller的@Validated中声明组即可:

 //原本的: public Object Insert(@Validated @RequestBody User user,BindingResult result){}
 //现在的插入的方法,声明采用插入校验组,即不约束id非空
 public Object Insert(@Validated(User.InsertUserGroup.class) @RequestBody User user,BindingResult result){}

另一种实现方式: 直接返回一个视图
ModelAndView对象同时包含了数据和视图

@Controller
public class ReturnController {

@GetMapping("/hello")
public ModelAndView hello(Model model) {

	ModelAndView mv = new ModelAndView();
	mv.addObject("name","李四");
	mv.addObject("age",20);
	//声明视图的名称
	mv.setViewName("hello");
	return mv;
	}
}

上传文件解析器

Apache Commons FileUpload库中有 CommonsMultipartResolver 处理类, SB3之后不再支持.

转向SB3自己的封装好的处理上传文件的接口 MultipartResolver,内部实现类 StandardServletMultipartResolver, 用于解析上传文件的请求

StandardServletMultipartResolver 内部封装了读取 POST 其中体的请求数据,也就是文件内容。我们现在只需要在 Controller 的方法加入形参@RequestParam(“”) MultipartFile multipartFile。 MultipartFile 表示上传的文件,提供了方便的方法保存文件到磁盘

MultipartFile Api
在这里插入图片描述
对于SB3内置的MultipartFile的对象, 默认单个文件最大支持 1M,一次请求最大 10M。改变默认值,需要 application 修改配置项

spring: 
	servlet:
		multipart:
			max-file-size: 800B
			max-request-size: 5MB
			file-size-threshold: 0KB
#file-size-threshold 超过指定大小,直接写文件到磁盘,不在内存处理

实例

需求: 上传文件到服务器(电脑)
在这里插入图片描述

.1 创建模块

模块名UploadFile, 依赖选择Web和Lombok

.2 服务器(电脑)中创建文件夹存放上传文件

例如
E:/upload

.3 前端页面
<!--上传文件页面-->
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Title</title>
</head>
<body>
	<h3>上传文件</h3>
	<!--指定加密方式, 传输方法-->
	<form action="files" enctype="multipart/form-data" method="post">
		<!--表示一个上传文件, 其中name属性的值要和controller中RequestParam的值一致-->
		选择文件:<input type="file" name="upfile" > <br/>
		<input type="submit" value="上传文件">
	</form>
</body>
</html>
<!--上传成功页面-->
<!DOCTYPE html>
<html lang="en">
<body>
	<h3>项目首页,上传文件成功</h3>
</body>
</html>
<!--上传失败页面-->
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Title</title>
</head>
<body>
	<h3>上传文件错误</h3>
</body>
</html>

还有错误页面, 错误页面无需在后端中设置跳转. 默认跳转路径就是error/

.4 后端代码
package com.sunsplanter.uploadfile.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@Controller
public class UploadFileController {
    @PostMapping("/upload")
    public String upload(@RequestParam("upfile") MultipartFile multipartFile){
        Map<String,Object> info = new HashMap<>();
        try {
            if( !multipartFile.isEmpty()){
                info.put("上传文件参数名",multipartFile.getName());
                info.put("内容类型",multipartFile.getContentType());

                //处理文件扩展名,默认是unknown
                var ext = "unknown";
                var filename = multipartFile.getOriginalFilename();
                if(filename.indexOf(".") > 0){
                    ext = filename.substring(filename.indexOf(".") + 1);
                }
                var newFileName = UUID.randomUUID().toString() + ext;
                var path = "D:/upload/" + newFileName;
                info.put("上传后文件名称", newFileName );
                multipartFile.transferTo(new File(path));
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
//防止 刷新,重复上传
        return "redirect:/index.html";
    }
}

在这里插入图片描述
访问localhost:8080/upload.html即可
TBD: 正常操作是登录后跳转到该静态资源, 这里只演示上传文件, 简单起见.

  • 10
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值