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 拦截器使用
- 创建拦截器类 , 实现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");
}
}
- 在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());
}
}
- 拦截器的进一步配置
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)注解的异常处理类
-
声明异常处理控制器类
异常处理控制类,统一定义异常处理handler方法
/**
* projectName: com.sunsplanter.execptionhandler
*
* description: 全局异常处理器,内部可以定义异常处理Handler!
*
* @RestControllerAdvice = @ControllerAdvice + @ResponseBody
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
}
- 在异常处理控制器类中声明异常处理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">
除 数:<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>
步骤是 :
- 在实体类(Bean)属性中用用注解限定属性的取值范围
- 在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;
}
}
常用的校验注解就是非空和长度限定 , 其中存在三种非空 :
- @NotNull : 包装类型不为null
- @NotEmpty : 集合类型长度大于0 , 如Collection/Map/数组对象
- @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: 正常操作是登录后跳转到该静态资源, 这里只演示上传文件, 简单起见.