JSR303参数校验-SpringMVC

文章目录

  • JSR303技术标准简介
  • JSR303标准几个具体实现框架
    • validation-api
    • jakarta.validation-api
    • hibernate-validator
    • spring-boot-starter-validation
  • Spring Validation
  • javax.validation.constraints包下提供的注解
  • org.hibernate.validator.constraints包扩展的注解
  • 校验注解默认错误信息
  • @Valid、 @Validated
  • SpringBoot-JSR-303数据校验
    • @RequestParam/@PathVariable传参
    • @RequestBody参数校验
    • 级联属性数据校验
    • 分组校验
    • 自定义校验注解
    • 组合多个校验注解成自定义注解
  • 统一校验异常处理
    • ConstraintViolationException.class
    • MethodArgumentNotValidException.class
    • BindException.class
  • 注解@Pattern 常用校验
    • 正则基本语法符号
    • 使用小技巧
    • 常用的正则校验
  • 枚举类型参数校验
    • User对象标注@EnumValue
    • 自定义@EnumValue注解(注解名随缘)
    • 枚举类提供逻辑校验的方法
    • 实现@EnumValue的校验器
    • 总结使用步骤
  • @RequestBody参数校验实现原理
  • RequestParam/PathVariable参数校验实现原理
  • 基于XML的配置校验器

JSR303技术标准简介

  1. JCP(Java Community Process)为Java技术制定标准技术规范的机构,任何人都可以注册 JCP 网站,并且可以参与JSR(Java Specification Requests:Java规范提案)的评审,也可以提交自己的JSR
  2. Bean Validation是Java定义的一套基于注解的数据校验规范,出自JSR303,JSR349,JSR380规范提案.

JSR303标准几个具体实现框架

JSR规范提案只是提供了技术标准,并没有提供具体的实现。具体相关框架有validation-apijakarta.validation-apihibernate-validator

validation-api

Java在2009年的Java EE 6中发布了JSR303以及javax.validation包,对JSR303版本的Bean Validation作了接口规范, 并没有相关校验代码。

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>

jakarta.validation-api

jakarta.validation-api是由validation-api改名而来。2018-03-05年,开源组织Eclipse基金会宣布将Java EE(Enterprise Edition:企业版)被更名为Jakarta EE(雅加达)。这是Oracle将Java EE移交给Eclipse基金会后加强对Java品牌控制的措施。随着Java EE的改名,包名和路径名也做了相应修改,从上面的引用路径可以看出来。

<dependency>
    <groupId>jakarta.validation</groupId>
    <artifactId>jakarta.validation-api</artifactId>
    <version>2.0.2</version>
</dependency>

hibernate-validator

hibernate-validator与持久层框架Hibernate没有什么关系,hibernate-validator是Hibernate基金会下的一个项目,是对Bean Validation的具体实现,它提供了JSR380规范中所有内置constraint的实现,还加入了它自己的一些constraint实现。点开pom文件,会发现hibernate-validator依赖于validation-api(jakarta.validation-api)。所以我们使用数据验证的时候可以直接引用这个包即可。

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

spring-boot-starter-validation

对于SpringBoot项目,直接引用它提供的starter,这个starter内部依赖了hibernate-validator

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

image.png

Spring Validation

Java API规范(JSR303)定义了Bean校验的标准validation-api,但没有提供实现。hibernate validation是对这个规范的实现,并增加了校验注解如@Email、@Length等。Spring Validation是对hibernate validation的二次封装,用于支持spring mvc参数自动校验

javax.validation.constraints包下提供的注解

注解说明可用类型
@AssertFalse必须为falseboolean,Boolean
@AssertTrue必须为trueboolean,Boolean
@DecimalMax(value=,inclusive=)必须是一个数字,其值必须小于或等于指定的最大值,支持的类型:BigDecimal、BigInteger、CharSequence、byte、short、int、long及其包装类BigDecimal,BigInteger,CharSequence,byte,short,int,long,Byte,Short,Integer,Long
@DecimalMin(value=,inclusive=)被注释的元素必须是一个数字,其值必须大于或等于指定的最小值。null 值是有效的。
value:用 String 表示 BigDecimal 字符串的最小值。
inclusive:true 表示大于或等于指定的最小值,false 表示大于最小值。默认为 true。
BigDecimal,BigInteger,CharSequence,byte,short,int,long,Byte,Short,Integer,Long
@Digits(integer=, fraction=)带注释的元素必须是可接受范围内的数字。null 值是有效的。
integer:此数字所接受的最大整数的位数。
fraction:该数字所接受的最大小数的位数。
BigDecimal,BigInteger,CharSequence,byte,short,int,long,Byte,Short,Integer,Long
@Email(regexp=, flags={})检查指定的字符序列是否为有效的电子邮件地址。允许通过可选参数regexp和flags指定电子邮件必须匹配的附加正则表达式(包括正则表达式标志)。null 值是有效的。CharSequence
@Future被注释的元素必须是一个未来的日期时间。null 值是有效的。和@Past相同
@FutureOrPresent被注释的元素必须是一个现在或未来的日期时间。null 值是有效的。和@Past相同
@Max(value=)被注释的元素必须是一个数字,其值必须小于或等于指定的最大值。null 值是有效的。
value:指定元素必须小于或等于的 long 值。
BigDecimal,BigInteger,byte,short,int,long,Byte,Short,Integer,Long
@Min(value=)被注释的元素必须是一个数字,其值必须大于或等于指定的最小值。null 值是有效的。
value:指定元素必须大于或等于的 long 值。
BigDecimal,BigInteger,byte,short,int,long,Byte,Short,Integer,Long
@Negative必须是严格的负数(即0被认为是无效值), null 值是有效的。BigDecimal,BigInteger,byte,short,int,long,float,double,Byte,Short,Integer,Long,Float,Double
@NegativeOrZero被注释的元素必须是负数或 0。null 值是有效的。BigDecimal,BigInteger,byte,short,int,long,float,double,Byte,Short,Integer,Long,Float,Double
@NotBlank被注释的元素不能是 null,并且必须包含至少一个非空白字符。CharSequence
@NotEmpty被注释的元素不能是 null 或空。CharSequence:计算字符序列的长度。
Collection:计算集合大小。
Map:计算 map 的大小。
Array:计算数组长度。
@NotNull不能为null任何类型
@Null必须为null任何类型
@Past被注释的元素必须是一个过去的日期时间。null 值是有效的。java.util.Date
java.util.Calendar
java.time.Instant
java.time.LocalDate
java.time.LocalDateTime
java.time.LocalTime
java.time.MonthDay
java.time.OffsetDateTime
java.time.OffsetTime
java.time.Year
java.time.YearMonth
java.time.ZonedDateTime
java.time.chrono.HijrahDate
java.time.chrono.JapaneseDate
java.time.chrono.MinguoDate
java.time.chrono.ThaiBuddhistDate
@PastOrPresent被注释的元素必须是一个过去或现在的日期时间。null 值是有效的。和@Past相同
@Pattern(regexp=, flags={})被注释的元素必须匹配指定的正则表达式。正则表达式遵循 Java 正则表达式约定,见 Java.util.regex.Pattern。null 值是有效的。
regexp:要匹配的正则表达式。
flags:解析正则表达式时考虑的 Flag 枚举数组。
CharSequence
@Positive被注释的元素必须是一个严格的正数(即 0 被认为是无效的值)。null 值是有效的。BigDecimal,BigInteger,byte,short,int,long,float,double,Byte,Short,Integer,Long,Float,Double
@PositiveOrZero被注释的元素必须是正数或 0。null 值是有效的。BigDecimal,BigInteger,byte,short,int,long,float,double,Byte,Short,Integer,Long,Float,Double
@Size(min=, max=)被注释的元素大小必须介于最小和最大(闭区间)之间。null 值是有效的。
min:指定元素必须大于或等于的 int 值,默认为 0。
max:指定元素必须小于或等于的 int 值,默认为 int 类型的最大值。
CharSequence:计算字符序列的长度。
Collection:计算集合大小。
Map:计算 map 的大小。
Array:计算数组长度。
@URL校验URL的合法性,但是并不完善,例如:https://;https:www.baidu.com等校验不住借助它regex属性,指定正则表达式进行校验
^(http|https)😕/[a-zA-Z0-9]+([\\-\\.]{1}[a-zA-Z0-9]+)\\.[a-zA-Z]{2,5}(:[0-9]{1,5})?(\\/.)?$

org.hibernate.validator.constraints包扩展的注解

  • 具体的注解可到org.hibernate.validator.constraints包下面自行查看
  • 校验实现可以到org.hibernate.validator.internal.constraintvalidators包下面自行查看。
    • 其中bv包下是对javax.validation下注解的实现,
    • hv包下是对org.hibernate.validator下注解的实现
注解描述
URL字符串是否为URL
CNPJ验证CNPJ(巴西企业纳税人注册号)
CPF验证CPF(巴西个人纳税人登记号)
TituloEleitoral巴西选民身份证号码
NIP验证是否为NIP编号(9位波兰增值税标识号)
PESEL验证PESEL(波兰国家识别号)
INN俄罗斯纳税人识别号
REGON验证REGON编号(9/14位波兰纳税人识别号)
DurationMax必须小于或等于DurationMax#nanos()、DurationMax#millis()等
DurationMin必须大于于或等于DurationMax#nanos()、DurationMax#millis()等
CodePointLength验证字符序列的长度是否在最小值和最大值之间
ConstraintComposition应用于组合约束注释的所有约束的布尔运算符
CreditCardNumber带注释的元素必须表示有效的信用卡号码,这是Luhn算法的实现,它的目的是检查用户的错误,而不是信用卡的有效性
Currency必须是正确的
EAN列是否为有效数字,并验证校验位
ISBN验证ISBN字符是否有效,号码的长度和校验位都经过验证
Length验证字符串是否在最小值和最大值之间
LuhnCheckLuhn算法检查约束
Mod10Check允许验证一系列数字是否通过Mod10校验和算法。经典的Mod10是通过对每个奇数的数字求和来计算的数字(从右到左)值乘以{@code multiplier}。
Mod11Check验证一系列数字是否通过Mod11校验和算法。对于最常见的Mod11变量,总和计算是通过将从最右边的数字(不包括校验数字)到最左边的数字。重量从2开始,每个数字增加1。然后将结果用于使用{@code11-(sum%11)}计算校验位
Normalized验证字符序列是否为规范化形式。可以通过设置规范化策略来验证规范化值
ParameterScriptAssert方法级约束,用于根据带注释的方法或构造函数。此约束可用于实现依赖于带注释对象的多个参数的验证例程
Range必须在适当的范围内。应用于数值或字符串数值的表示
ScriptAssert类级约束,用于根据带注释的元素。此约束可用于实现验证依赖于带注释元素的多个属性的例程
UniqueElements验证所提供的{@link Collection}中的每个对象都是唯一的,即我们在其中找不到2个相等的元素

校验注解默认错误信息

每个注解都有默认错误提示,如果没有写message属性的话则默认显示文件中配置的提示信息,我们用的是简体中文的ValidationMessages_zh_CN.properties文件,文件路径:

hibernate-validator-6.1.6.Final.jar!\org\hibernate\validator\ValidationMessages_zh_CN.properties

默认提示息如下所示:

javax.validation.constraints.AssertFalse.message     = 必须为 false
javax.validation.constraints.AssertTrue.message      = 必须为 true
javax.validation.constraints.DecimalMax.message      = 必须小于 ${inclusive == true ? 'or equal to ' : ''}{value}
javax.validation.constraints.DecimalMin.message      = 必须大于 ${inclusive == true ? 'or equal to ' : ''}{value}
javax.validation.constraints.Digits.message          = 数字值超出了边界(期望 <{integer} digits>.<{fraction} digits>)
javax.validation.constraints.Email.message           = 必须为格式规范的电子邮件地址
javax.validation.constraints.Future.message          = 必须是未来的日期
javax.validation.constraints.FutureOrPresent.message = 必须是现在或将来的日期
javax.validation.constraints.Max.message             = 必须小于或等于 {value}
javax.validation.constraints.Min.message             = 必须大于或等于 {value}
javax.validation.constraints.Negative.message        = 必须小于 0
javax.validation.constraints.NegativeOrZero.message  = 必须小于或等于 0
javax.validation.constraints.NotBlank.message        = 不得为空白
javax.validation.constraints.NotEmpty.message        = 不得为空
javax.validation.constraints.NotNull.message         = 不得为 null
javax.validation.constraints.Null.message            = 必须为 null
javax.validation.constraints.Past.message            = 必须是过去的日期
javax.validation.constraints.PastOrPresent.message   = 必须是过去或现在的日期
javax.validation.constraints.Pattern.message         = 必须与 "{regexp}" 匹配
javax.validation.constraints.Positive.message        = 必须大于 0
javax.validation.constraints.PositiveOrZero.message  = 必须大于或等于 0
javax.validation.constraints.Size.message            = 大小必须在 {min} 和 {max} 之间

org.hibernate.validator.constraints.CreditCardNumber.message        = 无效信用卡卡号
org.hibernate.validator.constraints.Currency.message                = 无效货币(必须为 {value} 之一)
org.hibernate.validator.constraints.EAN.message                     = 无效 {type} 条形码
org.hibernate.validator.constraints.Email.message                   = 电子邮件地址格式不规范
org.hibernate.validator.constraints.ISBN.message                    = 无效 ISBN
org.hibernate.validator.constraints.Length.message                  = 长度必须介于 {min} 与 {max} 之间
org.hibernate.validator.constraints.CodePointLength.message         = 长度必须介于 {min} 与 {max} 之间
org.hibernate.validator.constraints.LuhnCheck.message               = ${validatedValue} 的校验位无效,Luhn Modulo 10 校验和失败
org.hibernate.validator.constraints.Mod10Check.message              = ${validatedValue} 的校验位无效,Modulo 10 校验和失败
org.hibernate.validator.constraints.Mod11Check.message              = ${validatedValue} 的校验位无效,Modulo 11 校验和失败
org.hibernate.validator.constraints.ModCheck.message                = ${validatedValue} 的校验位无效,{modType} 校验和失败
org.hibernate.validator.constraints.NotBlank.message                = 可能不为空
org.hibernate.validator.constraints.NotEmpty.message                = 可能不为空
org.hibernate.validator.constraints.ParametersScriptAssert.message  = 脚本表达式 "{script}" 未求值为 true
org.hibernate.validator.constraints.Range.message                   = 必须介于 {min} 与 {max} 之间
org.hibernate.validator.constraints.SafeHtml.message                = 可能具有不安全的 HTML 内容
org.hibernate.validator.constraints.ScriptAssert.message            = 脚本表达式 "{script}" 未求值为 true
org.hibernate.validator.constraints.UniqueElements.message          = 必须仅包含唯一元素
org.hibernate.validator.constraints.URL.message                     = 必须为有效 URL

org.hibernate.validator.constraints.br.CNPJ.message                 = 无效巴西企业纳税人登记号 (CNPJ)
org.hibernate.validator.constraints.br.CPF.message                  = 无效巴西个人纳税人登记号 (CPF)
org.hibernate.validator.constraints.br.TituloEleitoral.message      = 无效巴西投票人身份证号

org.hibernate.validator.constraints.pl.REGON.message                = 无效波兰纳税人识别号 (REGON)
org.hibernate.validator.constraints.pl.NIP.message                  = 无效 VAT 识别号 (NIP)
org.hibernate.validator.constraints.pl.PESEL.message                = 无效波兰身份证号 (PESEL)

org.hibernate.validator.constraints.time.DurationMax.message        = 必须短于 ${inclusive == true ? ' or equal to' : ''}${days == 0 ? '' : days == 1 ? ' 1 day' : ' ' += days += ' days'}${hours == 0 ? '' : hours == 1 ? ' 1 hour' : ' ' += hours += ' hours'}${minutes == 0 ? '' : minutes == 1 ? ' 1 minute' : ' ' += minutes += ' minutes'}${seconds == 0 ? '' : seconds == 1 ? ' 1 second' : ' ' += seconds += ' seconds'}${millis == 0 ? '' : millis == 1 ? ' 1 milli' : ' ' += millis += ' millis'}${nanos == 0 ? '' : nanos == 1 ? ' 1 nano' : ' ' += nanos += ' nanos'}
org.hibernate.validator.constraints.time.DurationMin.message        = 必须长于 ${inclusive == true ? ' or equal to' : ''}${days == 0 ? '' : days == 1 ? ' 1 day' : ' ' += days += ' days'}${hours == 0 ? '' : hours == 1 ? ' 1 hour' : ' ' += hours += ' hours'}${minutes == 0 ? '' : minutes == 1 ? ' 1 minute' : ' ' += minutes += ' minutes'}${seconds == 0 ? '' : seconds == 1 ? ' 1 second' : ' ' += seconds += ' seconds'}${millis == 0 ? '' : millis == 1 ? ' 1 milli' : ' ' += millis += ' millis'}${nanos == 0 ? '' : nanos == 1 ? ' 1 nano' : ' ' += nanos += ' nanos'}

@Valid、 @Validated

注解完整类型描述
@Validjavax.validation.Valid标记用于验证级联的属性、方法参数或方法返回类型。在验证属性、方法参数或方法返回类型时,将验证在对象及其属性上定义的约束。
@Validatedorg.springframework.validation.annotation.ValidatedSpring 提供的扩展注解,可以添加在类、方法参数、普通方法上,表示它们需要进行约束校验,可以很方便的用于分组校验。

SpringBoot-JSR-303数据校验

@RequestParam/@PathVariable传参

GET请求一般会使用requestParam/PathVariable传参。如果参数比较多(比如超过6个),还是推荐使用DTO对象接收。否则,推荐将一个个参数平铺到方法入参中。在这种情况下,必须在Controller类上标注@Validated注解,并在入参上声明约束注解(如@Min等)。如果校验失败,会抛出ConstraintViolationException异常。代码示例如下

@RestController
@Validated
public class GroupController {

	@PostMapping("/audit/flow")
    public String startAuditFlow(@NotBlank(message = "申请ID不能为空") String applyRecordId) {
        // TODO
    }
    
}


@RequestBody参数校验

  1. POST、PUT请求一般会使用@RequestBody传递参数,这种情况下,后端使用VO对象进行接收。只要给VO对象加上@Validated注解就能实现自动参数校验。比如,有一个保存User的接口,要求userName长度是2-10,account和password字段长度是6-20。
  2. 如果校验失败,会抛出MethodArgumentNotValidException异常,Spring默认会将其转为400(Bad Request)请求。
@Data
public class UserDTO {

    private Long userId;

    @NotNull
    @Length(min = 2, max = 10)
    private String userName;

    @NotNull
    @Length(min = 6, max = 20)
    private String account;

    @NotNull
    @Length(min = 6, max = 20)
    private String password;
}


在方法参数上声明校验注解, 这种情况下,使用@Valid和@Validated都可以

@PostMapping("/save")
public Result saveUser(@RequestBody @Validated UserDTO userDTO) {
    // 校验通过,才会执行业务逻辑处理
    return Result.ok();
}

级联属性数据校验

1、如果User对象的级联属性address也需要校验; 只需在对应的address属性上添加@Valid注解即可,如下的代码片段

import lombok.Data;

import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;

@Data
public class User {

    @NotEmpty(message = "人员姓名不能为空")
    private String name;
    
    private String id;

    // 注意 @Valid 注解的标注位置
    private @Valid Address address;
}


// ===================================级联的地址对象================================

import lombok.Data;

import javax.validation.constraints.NotEmpty;

@Data
public class Address {

    @NotEmpty(message = "住址所在省份名称不能为空")
    private String province;
}

2、 使用@Validated或者@Valid注解开启SpringMVC在进行数据绑定的同时进行数据校验

@RestController
public class UserController {

    @PostMapping("/user/create")
    public Result<Object> createUser(@RequestBody @Validated User user) {
        return Result.of().setCode("2000").setMessage("成功").setData(user);
    }
}

分组校验

不同场景下的不同请求使用同一个Model对象接受请求参数时,存在不同的请求需要对Model对象的同一个属性制定不同的校验逻辑;比如上面的User对象的id属性;创建User对象的时候id是非必传;但是在更新User对象时id属性是必需的

1、 单纯的声明两个接口,充当分组

/**
 * @User 请求模型参数校验分组:创建用户时指定
 */
interface CreateUserValidGroup {
    // 不需要任何定义
}

/**
 * @User 请求模型参数校验分组:更新用户时指定
 */
interface UpdateUserValidGroup {
    // 不需要任何的定义
}

2、对标注的字段进行分组标记,使用校验注解的groups属性

@Data
public class User {

    @NotEmpty(message = "人员姓名不能为空")
    private String name;

    /**
     * 分组校验:创建用户的时候id是空的;用户信息更新的时候id是必录的
     * @Blank是自定义校验注解
     */
    @NotEmpty(message = "用户Id不能为空,在用户进行数据更新的时候",groups = {UpdateUserValidGroup.class})
    @Blank(message = "用户Id必须为空,在创建用户的时候",groups = {CreateUserValidGroup.class})
    private String id;

    private @Valid Address address;
}

3、使用@Validated告诉SpringMVC使用哪组校验进行数据校验

@RestController
public class UserController {

    @PostMapping("/user/create")
    public Result<Object> createUser(@RequestBody @Validated({CreateUserValidGroup.class}) User user) {
        return Result.of().setCode("2000").setMessage("成功").setData(user);
    }

    @PostMapping("/user/update")
    public Result<Object> modifyUser(@RequestBody @Validated({UpdateUserValidGroup.class}) User user) {
        return Result.of().setCode("2000").setMessage("成功").setData(user);
    }
}

自定义校验注解

有时候框架提供的约束注解并不能满足我们的需求,所以需要自定义合适的校验规则来满足自己的校验需求。

自定义注解用到的注解:

  • @Target:指定此注解支持的元素类型,比如:FIELD(属性)、METHOD(方法)等;
    
  • @Rentention(RUNTIME):指定此类型的注解将在运行时通过反射方式使用;
    
  • @Constraint(validatedBy = { }):标记注解的类型为约束,指定注解所使用的验证器(写验证逻辑的类),如果约束可以用在多种数据类型中,则每种数据类型对应一个验证器。
    
  • @Documented:表示带有某种类型的注释将由 javadoc 和类似工具记录;
    
  • @Repeatable(List.class):指示注解可以在相同的位置重复多次,通常具有不同的配置。List 包含注解类型。
    

Bean Validation API 规范要求任何约束注解都需要定义以下属性:

  1. message:定制化的提示信息,主要是从ValidationMessages.properties里提取,也可以依据实际情况进行定制。
    
  2. groups:用于将约束注解进行分组,不同组会执行不同的校验操作。
    
  3. payload:主要是针对Bean的,使用不多,未做详细了解。
    

为每一个自定义注解添加指定的校验器

@Documented
@Constraint(
    	// 指定解析该注解的类
        validatedBy = {BlankValidator.class}
)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Blank {
    String message() default "{com.idooy.param.constraint.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

对自定义的校验注解进行解析的类BlankValidator.class,需要实现特定的接口 ConstraintValidator

import org.springframework.util.StringUtils;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class BlankValidator implements ConstraintValidator<Blank,String> {

    @Override
    public void initialize(Blank constraintAnnotation) {
        ConstraintValidator.super.initialize(constraintAnnotation);
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return !StringUtils.hasText(value);
    }
}

组合多个校验注解成自定义注解

@Min(value = 18, message = "年龄最小不能小于18")
@Max(value = 120, message = "年龄最大不能超过120")
@Constraint(validatedBy = {})//不指定校验器
@Documented
@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomAgeValidator {
    String message() default "年龄大小必须大于18并且小于120";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

在创建完自定义注解后,我们在User类的age属性上使用自定义组合注解。

public class User {
    @NotBlank(message = "姓名不允许为空")
    @Length(min = 2, max = 10, message = "姓名长度错误,长度长度2-10!")
    private String name;

//    @NotNull(message = "年龄不能为空")
//    @Min(value = 18,message = "年齡最小不能小于18")
    @CustomAgeValidator
    private int age;

    @NotBlank(message = "地址不能为空")
    private String address;

    @Pattern(regexp = "^1(3[0-9]|5[012356789]|7[1235678]|8[0-9])\\d{8}$", message = "手机号格式错误")
    private String phone;

    @Email(message = "邮箱格式错误")
    private String email;

}

统一校验异常处理

ConstraintViolationException.class

抛出的场景:普通参数(非 java bean)校验出错时,会抛出 ConstraintViolationException 异常 比如:getUser(@NotBlank(message = "id不能为空") String id),GET请求一般会使用requestParam/PathVariable传参 ,将一个个参数平铺到方法入参中。在这种情况下,必须在Controller类上标注@Validated注解,并在入参上声明约束注解(如@Min等)。如果校验失败,会抛出ConstraintViolationException异常

@RequestMapping("/api/user")
@RestController
@Validated
public class UserController {
    // 路径变量
    @GetMapping("{userId}")
    public Result detail(@PathVariable("userId") @Min(10000000000000000L) Long userId) {
        // 校验通过,才会执行业务逻辑处理
        return Result.ok(userDTO);
    }

    // 查询参数
    @GetMapping("getByAccount")
    public Result getByAccount(@Length(min = 6, max = 20) @NotNull String  account) {
        // 校验通过,才会执行业务逻辑处理
        return Result.ok(userDTO);
    }
}
    /**
     * JSR303校验异常
     * 普通参数(非 java bean)校验出错时,会抛出 ConstraintViolationException 异常
     * @param ex
     * @return
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public R constraintViolationException(ConstraintViolationException ex){
        Set<ConstraintViolation<?>> violations = ex.getConstraintViolations();
        String message = violations.stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";"));
        return R.error(message);
    }

MethodArgumentNotValidException.class

抛出的场景:RestFul风格下POST、PUT请求一般会使用JSON格式的请求体绑定到VO对象上,并且使用@RequestBody注解。这种情况下校验参数失败时抛出MethodArgumentNotValidException

@PostMapping("/save")
public Result saveUser(@RequestBody @Validated UserDTO userDTO) {
    // 校验通过,才会执行业务逻辑处理
    return Result.ok();
}
    /**
     * JSR303校验异常
     * RestFul风格下POST、PUT请求一般会使用JSON格式的请求体绑定到VO对象上,
     * 并且使用@RequestBody注解。这种情况下校验参数失败时抛出MethodArgumentNotValidException
     * @param e
     * @return
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public R methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
        BindingResult bindingResult = e.getBindingResult();
        FieldError fieldError = bindingResult.getFieldError();
        if (fieldError != null) {
            return R.error(fieldError.getField() + " : " + fieldError.getDefaultMessage());
        }
        // 如果校验注解标注在类上,比如@PasswordMatches;此时FieldError就一定为null;
        // 所以可以这样获取错误信息
        String err = bindingResult.getAllErrors().get(0).getDefaultMessage();
        return R.error(err);
    }

BindException.class

抛出的场景: 入参绑定到javaBean(json格式,或者表单提交都是可以绑定到VO对象上),但是没有标注@RequestBody注解,示例代码如下:

@RestController
    @RequestMapping("/member")
    public class MemberController {

        @PostMapping("/build")
        public Result buildMember(@Valid Member member) {
            return Result.of().setData(member);
        }
    }

统一的异常处理代码片段

        /**
     * JSR303校验异常
     * 抛出的场景: 入参绑定到javaBean(json格式,或者表单提交都是可以绑定到VO对象上),
     * 但是没有标注@RequestBody注解,示例代码如下
     * @param ex
     * @return
     */
    @ExceptionHandler(BindException.class)
    public R bindExceptionHandle(BindException ex){
        ObjectError objectError = ex.getAllErrors().get(0);
        String defaultMessage = objectError.getDefaultMessage();
        return R.error(defaultMessage);
    }

注解@Pattern 常用校验

正则基本语法符号

首先来从基本语法符号开始:

  • ^ 表示匹配字符串的开始位置 (例外 用在中括号中[ ] 时,可以理解为取反,表示不匹配括号中字符串)
  • $ 表示匹配字符串的结束位置
  • * 表示匹配 零次到多次
  • + 表示匹配 一次到多次 (至少有一次)
  • ? 表示匹配零次或一次
  • . (圆点)表示匹配单个字符
  • | 表示为或者,两项中取一项(可用于两个正则之间)
  • ( ) 小括号表示匹配括号中全部字符
  • [ ] 中括号表示匹配括号中一个字符 范围描述 如[0-9 a-z A-Z]
  • { } 大括号用于限定匹配次数 如 {n}表示匹配n个字符 {n,}表示至少匹配n个字符 {n,m}表示至少n,最多m
  • \ 转义字符 如上基本符号匹配都需要转义字符 如 *表示匹配*号
  • \w 表示英文字母和数字 \W 非字母和数字
  • \d 表示数字 \D 非数字
  • \s表示空字符串
  • 当使用-时,需要放在最后一个位置,否则会被当成数字和字母之间区间的标识

使用小技巧

  1. 首先是首尾匹配,那么就要使用^和$,使用*或者\的时候,就要使用转义字符了(下面是在java中使用的,转义符号需要两个斜杠配合使用才好使)。
  2. 然后是长度限制时,可以使用大括号{ } {n}表示匹配n个字符 {n,}表示至少匹配n个字符 {n,m}表示至少n,最多m

常用的正则校验

手机号:
"^1(3[0-9]|5[012356789]|7[1235678]|8[0-9])\\d{8}$"

枚举类型参数校验

接口中某个常量参数值,需要映射业务枚举类中的某个实例,比如,审核状态枚举类定义了四个实例 “0”,“待审核”、“1”,“审核中”、“2”,“审核通过”、“3”,“审核驳回”;我们期望将参数中的常量进行验证并转换成对应的枚举实例。
思路

该功能仅仅满足如下需求:请求参数的常量值依赖枚举类型进行验证是否合法;并不能将请求参数常量直接映射成对应的枚举实例。如果期望‘验证+映射’功能可参看接下来的另一篇文章

  1. VO对象待校验的常量字段类型就用String或者Integer类型接。因为直接使用具体的枚举类型接就是类型转换器或者jackson的反序列化操作,难度较大(另起一文)。
  2. 枚举类中定义方法自行判断常量值是否可以映射具体的枚举实例。
  3. 注解校验器中使用反射调用第二步提供的方法。

具体的实现过程如下;

controllerc层Handler入参还是使用User;所以其中分组校验功能可以不用关注

    @PostMapping("/user/update")
    public Result<Object> modifyUser(@RequestBody @Validated({UpdateUserValidGroup.class}) User user) {
        
    }

User对象标注@EnumValue

其中的approvalStatus就是此次校验特别关注的字段

@Data
public class User {

    @NotEmpty(message = "人员姓名不能为空")
    private String name;

    /**
     * 分组校验:创建用户的时候id是空的;用户信息更新的时候id是必录的
     */
    @NotEmpty(message = "用户Id不能为空,在用户进行数据更新的时候",groups = {UpdateUserValidGroup.class})
    @Blank(message = "用户Id必须为空,在创建用户的时候",groups = {CreateUserValidGroup.class})
    private String id;

    @EnumValue(enumClass = ApprovalStatusEnums.class,groups = {UpdateUserValidGroup.class, CreateUserValidGroup.class})
    private String approvalStatus;
    private @Valid Address address;
}

接下来就是EnumValue注解类的定义,以及它所对应的校验器类EnumValueValidator

自定义@EnumValue注解(注解名随缘)

package com.idooy.param;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 校验当前被标注的字段传入的值是否可以正确映射为指定的枚举类型
 *
 * @author duheng
 */
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumValueValidator.class)
public @interface EnumValue {

    String message() default "com.idooy.param: invalid enum type value";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    /**
     * 当前被校验的字段值,要和哪个枚举类型进行映射
     * @return
     */
    Class<? extends Enum<?>> enumClass();

    /**
     * 指定枚举类中的一个存在的方法名:验证参数是否符合要求
     * @return
     */
    String enumMethod() default "isValidEnum";
}

枚举类提供逻辑校验的方法

枚举中需要提供一个可供业务逻辑判断的方法,@EnumValue默认执行isValidEnum方法。

package com.idooy.param;

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.Arrays;
import java.util.Objects;

/**
 * @author duheng
 */
@AllArgsConstructor
@Getter
public enum ApprovalStatusEnums {

    /**
     * 待审核
     */
    TO_BE_REVIEW("0","待审核"),
    /**
     * 审核中
     */
    REVIEWING("1","审核中"),

    /**
     * 业务员审核通过--->用户加入自定义群组的申请
     */
    REVIEW_AGREE("2","审核通过"),

    /**
     * 业务员审核驳回--->用户加入自定义群组的申请
     */
    REVIEW_REJECT("3","审核驳回");

    private String value;

    private String message;


    /**
     * 获取当前枚举的有效值
     * @param code
     * @return
     */
    public static boolean isValidEnum(String code) {
        return Arrays.stream(ApprovalStatusEnums.values())
                .anyMatch(o -> Objects.equals(o.getValue(), code));
    }
}

如果不使用默认的方法名isValidEnum,就可以在标注@EnumValue时指定方法名

实现@EnumValue的校验器

package com.idooy.param;

import org.springframework.util.StringUtils;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

/**
 *
 * @author duheng
 */
public class EnumValueValidator implements ConstraintValidator<EnumValue, Object> {
    private Class<? extends Enum<?>> enumClass;
    private String enumMethod;

    @Override
    public void initialize(EnumValue enumValue) {
        enumMethod = enumValue.enumMethod();
        enumClass = enumValue.enumClass();
    }

    /**
     *
     * @param value
     * @param constraintValidatorContext
     * @return
     */
    @Override
    public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
        // 对 null 或者 空字符串 不生效
        if (value == null) {
            return Boolean.TRUE;
        }
        if (value instanceof String && !StringUtils.hasText(value.toString())) {
            return Boolean.TRUE;
        }

        Class<?> valueClass = value.getClass();
        try {
            Method method = enumClass.getMethod(enumMethod, valueClass);
            if (!Boolean.TYPE.equals(method.getReturnType()) && !Boolean.class.equals(method.getReturnType())) {
                throw new RuntimeException(
                        String.format("%s method return is not boolean type in the %s class", enumMethod, enumClass));
            }

            if (!Modifier.isStatic(method.getModifiers())) {
                throw new RuntimeException(
                        String.format("%s method is not static method in the %s class", enumMethod, enumClass));
            }

            Boolean result = (Boolean) method.invoke(null, value.toString());
            return result == null ? false : result;
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            throw new RuntimeException(e);
        } catch (NoSuchMethodException | SecurityException e) {
            throw new RuntimeException(
                    String.format("This %s(%s) method does not exist in the %s", enumMethod, valueClass, enumClass), e);
        }
    }
}

总结使用步骤

如上;在完成EnumValue.javaEnumValueValidator.java编写后,该如何使用自定义注解@EnumValue完成String类型到枚举类型映射校验呢?

  1. @EnumValue对null和空串不生效;约定,入参最好是String或者Integer
  2. 保证枚举类中存在方法isValidEnum,要求:公共、静态、返回值为Boolean、入参有且只有一个String类型;也可自定义方法名,在使用@EnumValue时使用enumMethod指定
  3. 标注使用@EnumValue(enumClass = ApprovalStatusEnums.class)

@RequestBody参数校验实现原理

在spring-mvc中,RequestResponseBodyMethodProcessor是用于解析@RequestBody标注的参数以及处理@ResponseBody标注方法的返回值的。显然,执行参数校验的逻辑肯定就在解析参数的方法resolveArgument()中:

public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
    @Override
    public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

        parameter = parameter.nestedIfOptional();
        //将请求数据封装到DTO对象中
        Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
        String name = Conventions.getVariableNameForParameter(parameter);

        if (binderFactory != null) {
            WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
            if (arg != null) {
                // 执行数据校验
                validateIfApplicable(binder, parameter);
                if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
                    throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
                }
            }
            if (mavContainer != null) {
                mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
            }
        }
        return adaptArgumentIfNecessary(arg, parameter);
    }
}

可以看到,resolveArgument()调用了validateIfApplicable()进行参数校验。

protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
    // 获取参数注解,比如@RequestBody、@Valid、@Validated
    Annotation[] annotations = parameter.getParameterAnnotations();
    for (Annotation ann : annotations) {
        // 先尝试获取@Validated注解
        Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
        //如果直接标注了@Validated,那么直接开启校验。
        //如果没有,那么判断参数前是否有Valid起头的注解。
        if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
            Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
            Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
            //执行校验
            binder.validate(validationHints);
            break;
        }
    }
}

看到这里,大家应该能明白为什么这种场景下@Validated、@Valid两个注解可以混用。我们接下来继续看WebDataBinder.validate()实现。

@Override
public void validate(Object target, Errors errors, Object... validationHints) {
    if (this.targetValidator != null) {
        processConstraintViolations(
            //此处调用Hibernate Validator执行真正的校验
            this.targetValidator.validate(target, asValidationGroups(validationHints)), errors);
    }
}

最终发现底层最终还是调用了Hibernate Validator进行真正的校验处理。

RequestParam/PathVariable参数校验实现原理

上面提到的将参数一个个平铺到方法参数中,然后在每个参数前面声明约束注解的校验方式,就是方法级别的参数校验。实际上,这种方式可用于任何Spring Bean的方法上,比如Controller/Service等。其底层实现原理就是AOP,具体来说是通过MethodValidationPostProcessor动态注册AOP切面,然后使用MethodValidationInterceptor对切点方法织入增强

public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessorimplements InitializingBean {
    @Override
    public void afterPropertiesSet() {
        //为所有`@Validated`标注的Bean创建切面
        Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
        //创建Advisor进行增强
        this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
    }

    //创建Advice,本质就是一个方法拦截器
    protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
        return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
    }
}

接着看一下MethodValidationInterceptor

public class MethodValidationInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        //无需增强的方法,直接跳过
        if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
            return invocation.proceed();
        }
        //获取分组信息
        Class<?>[] groups = determineValidationGroups(invocation);
        ExecutableValidator execVal = this.validator.forExecutables();
        Method methodToValidate = invocation.getMethod();
        Set<ConstraintViolation<Object>> result;
        try {
            //方法入参校验,最终还是委托给Hibernate Validator来校验
            result = execVal.validateParameters(
                invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
        }
        catch (IllegalArgumentException ex) {
            ...
        }
        //有异常直接抛出
        if (!result.isEmpty()) {
            throw new ConstraintViolationException(result);
        }
        //真正的方法调用
        Object returnValue = invocation.proceed();
        //对返回值做校验,最终还是委托给Hibernate Validator来校验
        result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
        //有异常直接抛出
        if (!result.isEmpty()) {
            throw new ConstraintViolationException(result);
        }
        return returnValue;
    }
}

基于XML的配置校验器

<mvc:annotation-driven validator="validator"/>

<!-- 配置验证器 -->
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
  <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
</bean>

<!--  如果要在控制器方法的参数上直接使用验证注解进行修饰,需要该配置 -->
<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"/>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/370395.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

GPT在地学、GIS、气象、农业、生态、环境等领域中的高级应用

详情点击公众号&#xff1a;技术科研吧 链接&#xff1a;GPT在地学、GIS、气象、农业、生态、环境等领域中的高级应用 一开启大模型 1 开启大模型 1)大模型的发展历程与最新功能 2)大模型的强大功能与应用场景 3)国内外经典大模型&#xff08;ChatGPT、LLaMA、Gemini、DAL…

[Vulnhub靶机] DriftingBlues: 3

[Vulnhub靶机] DriftingBlues: 3靶机渗透思路及方法&#xff08;个人分享&#xff09; 靶机下载地址&#xff1a; https://download.vulnhub.com/driftingblues/driftingblues3.ova 靶机地址&#xff1a;192.168.67.19 攻击机地址&#xff1a;192.168.67.3 一、信息收集 1.…

leetcode206反转链表|详细算法讲解学习

题目 https://leetcode.cn/problems/reverse-linked-list/ 这道题对于刚开始学习数据结构和算法的人来说有点难&#xff0c;是入门的重要典型题目&#xff1b;但等数据结构入门之后&#xff0c;这就会是一道非常简单的题目了。 算法一&#xff08;算法正确但超出时间限制&am…

Linux|Grep 命令的 12 个实用示例

您是否曾经遇到过在文件中查找特定字符串或模式的任务&#xff0c;但不知道从哪里开始查找&#xff1f;那么&#xff0c;grep 命令可以拯救你&#xff01; grep 是一个功能强大的文件模式搜索器&#xff0c;每个 Linux 发行版都配备了它。如果出于某种原因&#xff0c;它没有安…

Django的web框架Django Rest_Framework精讲(一)

文章目录 Django Rest_Framework1. DRF介绍2.DRF特点3.环境安装与配置&#xff08;1&#xff09;DRF需要以下依赖&#xff08;2&#xff09;创建django项目 4.序列化器的使用&#xff08;1&#xff09;创建序列化器 5. 反序列化器使用 Django Rest_Framework 1. DRF介绍 Djan…

选择排序、冒泡排序----C语言数据结构

目录 引言 1.选择排序的实现1.1选择排序的时间复杂度2.冒泡排序的实现2.1冒泡排序的时间复杂度分析及优缺 引言 选择排序&#xff08;Selection Sort&#xff09;是一种简单直观的排序算法&#xff0c;它的基本思想是每次从未排序的元素中选择最小&#xff08;或最大&#xff…

VitePress-08-文档中代码组的使用

什么是代码组 代码组 : 就是代码块的集合。一个代码组中可以包含多个代码块。 效果 &#xff1a; 用页签的形式将不同的代码块分开展示。 代码组的语法格式 代码组的语法格式较为固定&#xff0c;如下 &#xff1a; ::: code-group代码块1的类型 [代码块1展示的页签名称]代码块…

全新 鸿蒙系统

一&#xff0c; 开发框架 基础 二&#xff0c; 官网地址 文档开发&#xff1a;华为HarmonyOS智能终端操作系统官网 | 应用设备分布式开发者生态 三&#xff0c;基础了解 鸿蒙系统是基于 js 和 ts 衍生出来的一个东西 要学 arkts 就要学习 js 和 ts 语法 四&#xff0c…

Avalonia学习(二十二)-数据库操作端

开始项目式的例子&#xff0c;但是不方便给大家贴代码了。 内容很多&#xff0c;只能演示一个界面&#xff0c;例子上传。 我不擅长界面美化和配色&#xff0c;有兴趣的可以继续完善&#xff0c;当前实现mysql。 最近所有样例的地址&#xff1a; GitHub - jinyuttt/Avalonia…

基于条纹投影的三维形貌与形变测量技术研究

▒▒本文目录▒▒ 一、 引言二、基于条纹投影轮廓术的形变测量实验2.1 实验光路2.2 实验结果 三、参考文献四、结论五、软硬件系统开发六、交流与合作 一、 引言 作为一种典型的三维形貌重建方法&#xff0c;条纹投影轮廓测量术&#xff08;Fringe Projection Profilometry&am…

java入门、环境配置及其特点介绍

目录 一、java语言的重要特点 二、java开发工具包&#xff08;JDK&#xff09;及其环境配置 三、java入门代码 四、Java运行机制 五、java学习方法 一、java语言的重要特点 java是面向对象的Java是健壮性的。Java具有强类型机制、异常处理、垃圾的自动收集等特点java语言是跨…

炼丹师必备平台,云上训练无压力—GpuMall智算云

说回作为一个“炼丹师”在选择炼丹平台的核心需求&#xff1a;【数据传输速率快慢】or【机器租用价格性价比】or【可租用机器类型多少】or【镜像版本选择多样性】or【机器稳定性】等等 是不是以上任何一个条件&#xff0c;都可能会影响你对平台的选择呢&#xff1f;纵观市面上…

python coding with ChatGPT 打卡第16天| 二叉树:完全二叉树、平衡二叉树、二叉树的所有路径、左叶子之和

相关推荐 python coding with ChatGPT 打卡第12天| 二叉树&#xff1a;理论基础 python coding with ChatGPT 打卡第13天| 二叉树的深度优先遍历 python coding with ChatGPT 打卡第14天| 二叉树的广度优先遍历 python coding with ChatGPT 打卡第15天| 二叉树&#xff1a;翻转…

曲线拟合、多项式拟合、最小二乘法

最近在做自车轨迹预测的工作&#xff0c;遇到 曲线拟合、多项式拟合、最小二乘法这些概念有点不清晰&#xff0c; 做一些概念区别的总结&#xff1a; 曲线拟合用于查找一系列数据点的“最佳拟合”线或曲线。 大多数情况下&#xff0c;曲线拟合将产生一个函数&#xff0c;可用于…

【数据结构与算法】(7)基础数据结构之双端队列的链表实现、环形数组实现示例讲解

目录 2.6 双端队列1) 概述2) 链表实现3) 数组实现习题E01. 二叉树 Z 字层序遍历-Leetcode 103 2.6 双端队列 1) 概述 双端队列、队列、栈对比 定义特点队列一端删除&#xff08;头&#xff09;另一端添加&#xff08;尾&#xff09;First In First Out栈一端删除和添加&…

网工内推 | 金融业网络安全岗,最高40K*15薪,CISP认证优先

01 国泰产险 招聘岗位&#xff1a;资深信息安全工程师 职责描述&#xff1a; 1、负责公司云平台业务系统的安全规划设计&#xff0c;协助业务系统制定安全解决方案&#xff1b; 2、负责建立公司信息安全标准&#xff0c;制定平台安全策略&#xff0c;安全加固&#xff0c;防范…

数据可视化 pycharts实现中国各省市地图数据可视化

自用版 数据格式如下&#xff1a; 运行效果如下&#xff1a; import pandas as pd from pyecharts.charts import Map, TreeMap, Timeline, Page, WordCloud from pyecharts import options as opts from pyecharts.commons.utils import JsCode from pyecharts.globals im…

js逆向-某验3代滑块验证码分析

声明 本文仅供学习参考&#xff0c;如有侵权可私信本人删除&#xff0c;请勿用于其他途径&#xff0c;违者后果自负&#xff01; 如果觉得文章对你有所帮助&#xff0c;可以给博主点击关注和收藏哦&#xff01; 插句个人内容&#xff1a;本人最近正在找工作&#xff0c;工作城…

alibabacloud学习笔记05(小滴课堂)

高并发下的微服务存在的问题 高并发下的微服务容错方案 介绍什么是分布式系统的流量防卫兵Sentinel 微服务引入Sentinel和控制台搭建 每个服务都加上这个依赖。 启动方式&#xff1a; 讲解AliababCloud微服务整合Sentinel限流配置实操 我们在order和video模块都加上。 分别启动…

第三百零六回

文章目录 1. 概念介绍2. 思路与方法2.1 实现思路2.2 实现方法 3. 示例代码4. 内容总结 我们在上一章回中介绍了"分享三个使用TextField的细节"沉浸式状态样相关的内容&#xff0c;本章回中将介绍如何创建单例模式.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1.…