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博客

相关推荐
程序员-珍23 分钟前
SpringBoot v2.6.13 整合 swagger
java·spring boot·后端
海里真的有鱼31 分钟前
好文推荐-架构
后端
骆晨学长1 小时前
基于springboot的智慧社区微信小程序
java·数据库·spring boot·后端·微信小程序·小程序
AskHarries1 小时前
利用反射实现动态代理
java·后端·reflect
Flying_Fish_roe2 小时前
Spring Boot-Session管理问题
java·spring boot·后端
hai405872 小时前
Spring Boot中的响应与分层解耦架构
spring boot·后端·架构
Adolf_19934 小时前
Flask-JWT-Extended登录验证, 不用自定义
后端·python·flask
叫我:松哥4 小时前
基于Python flask的医院管理学院,医生能够增加/删除/修改/删除病人的数据信息,有可视化分析
javascript·后端·python·mysql·信息可视化·flask·bootstrap
海里真的有鱼4 小时前
Spring Boot 项目中整合 RabbitMQ,使用死信队列(Dead Letter Exchange, DLX)实现延迟队列功能
开发语言·后端·rabbitmq
工业甲酰苯胺4 小时前
Spring Boot 整合 MyBatis 的详细步骤(两种方式)
spring boot·后端·mybatis