SpringMvc快速学习笔记下篇

上篇地址

上篇地址为:SpringMvc快速学习笔记 - 掘金 (juejin.cn)

web项目中的拦截器和过滤器

在web项目中我们往往需要一些过滤器对请求进行更细化的处理,来解决诸如登录验证,数据脱敏等等一系列重要的问题。


Servlet过滤器与Spring过滤器

我们知道在Servlet中,为我们准备了过滤器 ,而在Spring项目中 ,也为我们提供了更全面更细化 的过滤器(称为拦截器)

2者的共同点

  1. 拦截,都是对请求进行拦截

  2. 存在的目的都是对拦截的请求进行统一处理

  3. 都需要判定并放行

不同点

  1. 工作平台不同

    1. Filter只需要Servlet即可运行

    2. SpringMvc的拦截器必须在Spring框架下才能运行

  2. 拦截范围不同

    1. 拦截整个web项目
    2. SpringMvc只拦截自己管理的内容
  3. IOC 容器支持

    1. 过滤器:想得到 IOC 容器需要调用专门的工具方法,是间接的

    2. 拦截器:它自己就在 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个要求。

  1. 如果添加了拦截器而不配置过滤条件,则默认对所有请求生效!

  2. exclude必须是addPathPatterns的子集。

  3. 如果有多个拦截器,则需要为每个拦截器都配置过滤条件,他们不能共用!


多个拦截器的优先级问题(重点)

如果添加了多个拦截器,则先放入的拦截器优先级更高(优先级参考环绕式处理)。

通过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 标注值必须是格式正确的 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博客

相关推荐
苏打水com9 分钟前
数据库进阶实战:从性能优化到分布式架构的核心突破
数据库·后端
间彧1 小时前
Spring Cloud Gateway与Kong或Nginx等API网关相比有哪些优劣势?
后端
间彧1 小时前
如何基于Spring Cloud Gateway实现灰度发布的具体配置示例?
后端
间彧1 小时前
在实际项目中如何设计一个高可用的Spring Cloud Gateway集群?
后端
间彧1 小时前
如何为Spring Cloud Gateway配置具体的负载均衡策略?
后端
间彧1 小时前
Spring Cloud Gateway详解与应用实战
后端
EnCi Zheng3 小时前
SpringBoot 配置文件完全指南-从入门到精通
java·spring boot·后端
烙印6013 小时前
Spring容器的心脏:深度解析refresh()方法(上)
java·后端·spring
Lisonseekpan3 小时前
Guava Cache 高性能本地缓存库详解与使用案例
java·spring boot·后端·缓存·guava
4 小时前
JUC专题 - 并发编程带来的安全性挑战之同步锁
后端