上篇地址
上篇地址为:SpringMvc快速学习笔记 - 掘金 (juejin.cn)
web项目中的拦截器和过滤器
在web项目中我们往往需要一些过滤器对请求进行更细化的处理,来解决诸如登录验证,数据脱敏等等一系列重要的问题。
Servlet过滤器与Spring过滤器
我们知道在Servlet中,为我们准备了过滤器 ,而在Spring项目中 ,也为我们提供了更全面更细化 的过滤器(称为拦截器)
2者的共同点
-
拦截,都是对请求进行拦截
-
存在的目的都是对拦截的请求进行统一处理
-
都需要判定并放行
不同点
-
工作平台不同
-
Filter只需要Servlet即可运行
-
SpringMvc的拦截器必须在Spring框架下才能运行
-
-
拦截范围不同
- 拦截整个web项目
- SpringMvc只拦截自己管理的内容
-
IOC 容器支持
-
过滤器:想得到 IOC 容器需要调用专门的工具方法,是间接的
-
拦截器:它自己就在 IOC 容器中,所以可以直接从 IOC 容器中装配组件,也就是可以直接得到 IOC 容器的支持
-
如何选择(重点)
功能需要如果用 SpringMVC 的拦截器能够实现,就不使用过滤器。
Servlet拦截器的位置是在整个请求刚刚进入时就拦截的,放行也是最终放行
。 可以说Servlet拦截器的优先级更高,但不够细致。
而SpringMvc的拦截器因为SpringMvc本身是web项目的一部分
,所以其实算是包含在Servlet拦截器内部的
Spring过滤器实战
我们首先需要定义一个拦截器类 ,并继承HandlerInterceptor。
java
package com.atguigu.MyInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
public class MyInterceptor01 implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("处理器1preHandle");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("处理器1postHandle");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("处理器1 afterCompletion");
}
}
还需要将它放入配置类中 ,必须保证配置类实现了WebMvcConfigurer接口
,我们重写该方法,注意内部的默认重写与我们使用的registry是不同的!!!
kotlin
package com.atguigu.Config;
import com.atguigu.MyInterceptor.MyInterceptor01;
import com.atguigu.MyInterceptor.MyInterceptor02;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;
@EnableWebMvc
@ComponentScan("com.atguigu")
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
//添加拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
//TODO 使用registry而非默认
registry.addInterceptor(new MyInterceptor01())
.addPathPatterns("/user/**")
.excludePathPatterns("/user/hello");
}
}
Spring过滤器代码详解
java
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception{
System.out.println("处理器1preHandle");
return true;
}
@Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("处理器1postHandle");
}
@Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("处理器1 afterCompletion");
}
注意看上面的代码,我们详细讲一下。
第一个方法preHandle,它在Handler方法执行之前被调用,它必须返回一个Boolean值,为false则该请求不再处理,不放行。true则放行。
第二个方法postHandler,他在Handler方法之后执行,但无需返回Boolean,此处不再控制是否放行。
第三个方法afterComplete,他在DispatcherServlet处理完后执行
为什么要继承WebMvcConfigurer?
这是一个SpringMvc的默认配置类,它默认实现了很多方法,如果我们需要调整某些配置,可以直接选择重写。
通过URI配置拦截器(重点)
在代码2中我们发现addInterceptor方法本身是一个链式调用方法 ,我们可以添加相应的路径来配置对哪些路径进行处理,又对哪些路径进行过滤。这里有2个要求。
-
如果添加了拦截器而不配置过滤条件,则默认对所有请求生效!
-
exclude必须是addPathPatterns的子集。
-
如果有多个拦截器,则需要为每个拦截器都配置过滤条件,他们不能共用!
多个拦截器的优先级问题(重点)
如果添加了多个拦截器,则先放入的拦截器优先级更高(优先级参考环绕式处理)。
通过DispatcherServlet源码来看拦截器的执行过程
因为所有的请求都由DispatcherServlet统一管理(当然也需要调用其他的类),所以在DispatcherServlet的源码中我们可以看到拦截器的执行逻辑。
这部分代码放在DispatcherServlet的doDispatcher方法内
kotlin
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
上面这段代码是doDispatcher方法内的一段截取,内部的这个applyPreHandle方法会返回一个Boolean值,如果这个返回的Boolean值取反成立,则会导致doDispatcher方法直接返回不再继续执行。
kotlin
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {
HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
if (!interceptor.preHandle(request, response, this.handler)) {
this.triggerAfterCompletion(request, response, (Exception)null);
return false;
}
}
return true;
}
上面的代码就是这个至关重要的applyPreHandle方法,我们看它内部的这个if判断,它如果发现某个拦截器的preHandler返回了false(拦截器不放行),就会直接返回false,进而导致该请求被DispatcherServlet拦截并返回。
再回到doDispatcher方法,我们看看后置的拦截器是如何执行的。
bash
mappedHandler.applyPostHandle(processedRequest, response, mv);
这句代码就是后置处理器的执行方法,我们点开它的源码
java
void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) {
for(int i = this.interceptorIndex; i >= 0; --i) {
HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
try {
interceptor.afterCompletion(request, response, this.handler, ex);
} catch (Throwable var7) {
logger.error("HandlerInterceptor.afterCompletion threw exception", var7);
}
}
}
我们这里只关注这个for循环,我们会发现,与上面preHandler的源码不同,后置拦截器的for循序顺序是倒序,这就是为了达成《拦截器环绕执行》的目的而故意设计的!!!
参数校验注解jsr303
java为我们提供了Bean的校验注解标准 ,称为jsr303 。而hibernate框架给该标准提供了实现 。而Spring也支持该实现。我们引入相应的依赖
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>
使用方法概述
JSR 303 是 Java 为 Bean 数据合法性校验提供的标准框架,它已经包含在 JavaEE 6.0 标准中。JSR 303 通过在 Bean 属性上标注类似于 @NotNull、@Max 等标准的注解指定校验规则,并通过标准的验证接口对Bean进行验证。
注解 | 规则 |
---|---|
@Null | 标注值必须为 nul(用于包装类型) |
@NotNull | 标注值不可为 nulll(用于包装类型) |
@NotBlank | 用于标注字符串。表示不为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 地址 | |
@Length | 标注值字符串大小必须在指定的范围内 |
@NotEmpty | 校验集合类是否为空 |
@Range | 标注值必须在指定的范围内 |
Spring 4.0 版本已经拥有自己独立的数据校验框架,同时支持 JSR 303 标准的校验框架。Spring 在进行数据绑定时,可同时调用校验框架完成数据校验工作。在SpringMVC 中,可直接通过注解驱动 @EnableWebMvc 的方式进行数据校验。Spring 的 LocalValidatorFactoryBean 既实现了 Spring 的 Validator 接口,也实现了 JSR 303 的 Validator 接口。只要在Spring容器中定义了一个LocalValidatorFactoryBean,即可将其注入到需要数据校验的 Bean中。Spring本身并没有提供JSR 303的实现,所以必须将JSR 303的实现者的jar包放到类路径下。
配置 @EnableWebMvc后,SpringMVC 会默认装配好一个 LocalValidatorFactoryBean,通过在处理方法的入参上标注 @Validated 注解即可让 SpringMVC 在完成数据绑定后执行数据校验的工作。
代码实战
我们首先写一个pojo以及相应的请求
less
package com.atguigu.pojo;
import jakarta.validation.constraints.*;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;
import java.util.Date;
@Data
public class Emp {
@Length(max = 12,min = 12)
private String id;
@NotBlank
private String name;
@Min(value = 18,message = "年龄小于18岁啦~")
@Max(126)
@Digits(integer = 2,fraction = 0)
private int age;
@Email
private String email;
@Past()
private Date birthday;
}
//请求编写
package com.atguigu.Controller;
import com.atguigu.pojo.Emp;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("emp")
public class empController {
@PostMapping()
public Object saveEmp(@RequestBody @Validated Emp emp){
return "ok~~";
}
}
代码详解
我们在pojo中加入了相关注解,这相信大家都能看懂。多了一个Message ,大家应该能猜到,这是错误时的返回信息。
我们在Handler方法上加入了@Validated表示该对象需要被校验!
对返回结果进行处理
当我们发送一个错误的格式,尝试触发一下校验规则
他会返回一个页面,内部显示
由于被认为是客户端对错误(例如:畸形的请求语法、无效的请求信息帧或者虚拟的请求路由),服务器无法或不会处理当前请求。
我想说的是,我们在实际开发中,我们不可能直接返回一个这样的结果。
如果你之前设置了异常处理 ,你会发现。返回值与上面不同,它直接打印了异常信息
而在正常情况下,我们会希望它返回一个JSON对象,内部定义了相关的错误码以及我们定义的错误信息。
我们通过之前学过的自定义异常处理来解决这个问题
分析异常类型
我们查看日志信息
less
2023-10-17 15:00:56 667 [http-nio-8080-exec-15] WARN org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver - Resolved
[org.springframework.web.bind.MethodArgumentNotValidException:Validation failed for argument [0] in public java.lang.Object com.atguigu.Controller.empController.saveEmp(com.atguigu.pojo.Emp):
[Field error in object 'emp' on field 'age': rejected value [11];
codes [Min.emp.age,Min.age,Min.int,Min]; arguments
[org.springframework.context.support.DefaultMessageSourceResolvable: codes [emp.age,age]; arguments []; default message [age],18];
default message [年龄小于18岁啦~]] ]
总而言之就是它报了一个MethodArgumentNotValidException错 ,Validation failed for argument.....巴拉巴拉一大堆,找到对应的异常,问题就很好解决了(我们针对这个异常写个处理就完了)
arduino
//TODO 自定义javaBean字段校验异常处理
@ExceptionHandler(MethodArgumentNotValidException.class)
public Object javaBeanValidExceptionHandler(MethodArgumentNotValidException exception){
//这个方法会把错误信息搞成一个对象封装在一个集合里面(因为可能不止一个字段报错)
BindingResult bindingResult = exception.getBindingResult();
//写一个可拼接的字符串,用来拼接错误信息
StringBuilder errMsg = new StringBuilder("对象字段校验错误");
//这里看下面解释
bindingResult.getFieldErrors().forEach(fieldError -> errMsg
.append("字段为:"+fieldError.getField()+",错误信息:"+fieldError.getDefaultMessage()+","));
//new 一个Map放返回结果,我们随便定义一个code,大家不要纠结
Map<String, Object> map = new HashMap<>();
map.put("code",200);
//把拼接好的String放进去,然后返回给前端
map.put("message",errMsg.toString());
return map;
}
getFieldErrors()方法取出合集 ,我这里遍历了这个合集。getField()取出字段名称 ,然后开始字符串拼接,getDefaultMessage()方法取出我们顶一个错误信息 ,如果没有定义,他会有默认的。总之就是把错误的字段跟错误信息拼接成了一个长字符串里
,然后放map里返回给前端。
postman跑一下

字段校验高级知识
这东西记多了也会忘,有的我也没讲全,如果大家有任何疑问可以参考下面的链接,一个大佬列的很详细。 Validated数据校验,看这一篇就够了_validated校验_localhost65535的博客-CSDN博客