【SpringBoot】SpringBoot 优雅地校验参数

news2024/11/18 23:46:09

1、为什么要校验参数?

在日常的开发中,为了防止非法参数对业务造成影响,需要对接口的参数进行校验,以便正确性地入库。
例如:登录时,就需要判断用户名、密码等信息是否为空。虽然前端也有校验,但为了接口的安全性,后端接口还是有必要进行参数校验的。

同时,为了校验参数更加优雅,这里就介绍了 Spring Validation 方式。

Java API 规范(JSR303:JAVA EE 6 中的一项子规范,叫做 Bean Validation)定义了 Bean 校验的标准 validation-api,但没有提供实现。hibernate validation 是对这个规范的实现,并增加了校验注解。如:@Email、@Length。

JSR 官网

Hibernate Validator 官网

Spring Validation 是对 hibernate validation 的二次封装,用于支持 spring mvc 参数自动校验。

2、引入依赖

如果 spring-boot 版本小于 2.3.x,spring-boot-starter-web 会自动传入 hibernate-validator 依赖。如果 spring-boot 版本大于等于 2.3.x,则需要手动引入依赖。

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

对于 web 服务来说,为防止非法参数对业务造成影响,在 Controller 层一定要做参数校验的!大部分情况下,请求参数分为如下两种形式:

  • POST、PUT 请求,使用 @requestBody 接收参数
  • GET 请求,使用 @requestParam、@PathVariable 接收参数

接下来就简单介绍下吧~~~


3、@requestBody 参数校验

对于 POST、PUT 请求,后端一般会使用 @requestBody + 对象 接收参数。此时,只需要给对象添加 @Validated 或 @Valid 注解,即可轻松实现自动校验参数。如果校验失败,会抛出 MethodArgumentNotValidException 异常。

UserVo :添加校验注解

@Data
public class UserVo {

	private Long id;

    @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;
}

UserController :

@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping("/addUser")
    public String addUser(@RequestBody @Valid UserVo userVo) {
        return "addUser";
    }
}

或者使用 @Validated 注解:

@PostMapping("/addUser")
public String addUser(@RequestBody @Validated UserVo userVo) {
    return "addUser";
}

4、@requestParam、@PathVariable 参数校验

GET 请求一般会使用 @requestParam、@PathVariable 注解接收参数。如果参数比较多(比如超过 5 个),还是推荐使用对象接收。否则,推荐将一个个参数平铺到方法入参中。

在这种情况下,必须在 Controller 类上标注 @Validated 注解,并在入参上声明约束注解(如:@Min )。如果校验失败,会抛出 ConstraintViolationException 异常

@RestController
@RequestMapping("/user")
@Validated
public class UserController {

    @GetMapping("/getUser")
    public String getUser(@Min(1L) Long id) {
        return "getUser";
    }
}

5、统一异常处理

如果校验失败,会抛出 MethodArgumentNotValidException 或者 ConstraintViolationException 异常。在实际项目开发中,通常会用统一异常处理来返回一个更友好的提示

@RestControllerAdvice
public class ExceptionControllerAdvice {

    @ExceptionHandler({MethodArgumentNotValidException.class})
    @ResponseStatus(HttpStatus.OK)
    public String handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();
        StringBuilder sb = new StringBuilder("校验失败:");
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
        }
        String msg = sb.toString();
        return "参数校验失败" + msg;
    }

    @ExceptionHandler({ConstraintViolationException.class})
    public String handleConstraintViolationException(ConstraintViolationException ex) {
        return "参数校验失败" + ex;
    }

}

6、分组校验

在实际项目中,可能多个方法需要使用同一个类对象来接收参数,而不同方法的校验规则很可能是不一样的。这个时候,简单地在类的字段上加约束注解无法解决这个问题。因此,spring-validation 支持了分组校验的功能,专门用来解决这类问题。

如:保存 User 的时候,userId 是可空的,但是更新 User 的时候,userId 的值必须 >= 1L;其它字段的校验规则在两种情况下一样。这个时候使用分组校验的代码示例如下:

约束注解上声明适用的分组信息 groups

1:定义分组接口

public interface ValidGroup extends Default {
	// 添加操作
    interface Save extends ValidGroup {}
    // 更新操作
    interface Update extends ValidGroup {}
	// ...
}

为什么要继承 Default ?下文有。

2:给需要校验的字段分配分组

@Data
public class UserVo {
	
    @Null(groups = ValidGroup.Save.class, message = "id要为空")
    @NotNull(groups = ValidGroup.Update.class, message = "id不能为空")
    private Long id;

    @NotBlank(groups = ValidGroup.Save.class, message = "用户名不能为空")
    @Length(min = 2, max = 10)
    private String userName;

    @Email
    @NotNull
    private String email;
}

根据校验字段看:

  • id:分配分组:Save、Update。添加时,一定为 null;更新时,一定不为 null
  • userName:分配分组:Save。添加时,一定不能为空
  • email:分配分组:无。即:使用默认的分组

3:给需要校验的参数指定分组

@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping("/addUser")
    public String addUser(@RequestBody @Validated(ValidGroup.Save.class) UserVo userVo) {
        return "addUser";
    }

    @PostMapping("/updateUser")
    public String updateUser(@RequestBody @Validated(ValidGroup.Update.class) UserVo userVo) {
        return "updateUser";
    }
}

测试校验。

4:默认分组

如果 ValidGroup 接口 不继承 Default 接口,那么,将无法校验 email 字段(未分配分组);继承后,ValidGroup 就属于 Default 类型,即:默认分组/所以,可以对 email 校验

7、嵌套校验

必须要用 @Valid 注解

@Data
public class UserVo {

	@NotNull(groups = {ValidGroup.Save.class, ValidGroup.Update.class})
    @Valid
    private Address address;
}

8、自定义校验

案例一、自定义校验 加密id

假设我们自定义加密 id(由数字或者 a-f 的字母组成,32-256 长度)校验,主要分为两步:

1.自定义约束注解

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EncryptIdValidator.class}) // 自定义验证器
public @interface EncryptId {

    // 默认错误消息
    String message() default "加密id格式错误";
    // 分组
    Class<?>[] groups() default {};
    // 负载
    Class<? extends Payload>[] payload() default {};
}

2.编写约束校验器

public class EncryptIdValidator implements ConstraintValidator<EncryptId, String> {

    private static final Pattern PATTERN = Pattern.compile("^[a-f\\d]{32,256}$");

    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
        if (value != null) {
            Matcher matcher = PATTERN.matcher(value);
            return matcher.find();
        }
        return true;
    }
}

3.使用

@Data
public class UserVo {

    @EncryptId
    private String id;
}

案例二、自定义校验 性别只允许两个值

UserVo 类中的 sex 性别属性,只允许前端传递传 M,F 这2个枚举值,如何实现呢?

1.自定义约束注解

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {SexValidator.class})
public @interface SexValid {


    // 默认错误消息
    String message() default "value not in enum values";

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

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

    String[] value();
}

2.编写约束校验器

public class SexValidator implements ConstraintValidator<SexValid, String> {

    private List<String> sexs;

    @Override
    public void initialize(SexValid constraintAnnotation) {
        sexs = Arrays.asList(constraintAnnotation.value());
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
        if (StringUtils.isEmpty(value)) {
            return true;
        }
        return sexs.contains(value);
    }

}

3.使用

@Data
public class UserVo {

    @SexValid(value = {"F", "M"}, message = "性别只允许为F或M")
    private String sex;

}

4.测试

@GetMapping("/get")
private String get(@RequestBody @Validated UserVo userVo) {
    return "get";
}

9、实现校验业务规则

业务规则校验 指 接口需要满足某些特定的业务规则。举个例子:业务系统的用户需要保证其唯一性,用户属性不能与其他用户产生冲突,不允许与数据库中任何已有用户的用户名称、手机号码、邮箱产生重复。 这就要求在创建用户时需要校验用户名称、手机号码、邮箱是否被注册;编辑用户时不能将信息修改成已有用户的属性。

最优雅的实现方法应该是参考 Bean Validation 的标准方式,借助自定义校验注解完成业务规则校验。

1.自定义约束注解

首先我们需要创建两个自定义注解,用于业务规则校验:

  • UniqueUser:表示一个用户是唯一的,唯一性包含:用户名,手机号码、邮箱
  • NotConflictUser:表示一个用户的信息是无冲突的,无冲突是指该用户的敏感信息与其他用户不重合
@Documented
@Retention(RUNTIME)
@Target({FIELD, METHOD, PARAMETER, TYPE})
@Constraint(validatedBy = UserValidator.UniqueUserValidator.class)
public @interface UniqueUser {

    String message() default "用户名、手机号码、邮箱不允许与现存用户重复";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

}
@Documented
@Retention(RUNTIME)
@Target({FIELD, METHOD, PARAMETER, TYPE})
@Constraint(validatedBy = UserValidator.NotConflictUserValidator.class)
public @interface NotConflictUser {

    String message() default "用户名称、邮箱、手机号码与现存用户产生重复";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

}

2.编写约束校验器

想让自定义验证注解生效,需要实现 ConstraintValidator 接口。接口的第一个参数是 自定义注解类型,第二个参数是 被注解字段的类,因为需要校验多个参数,我们直接传入用户对象。 需要提到的一点是 ConstraintValidator 接口的实现类无需添加 @Component 它在启动的时候就已经被加载到容器中了。

public class UserValidator<T extends Annotation> implements ConstraintValidator<T, UserVo> {

    protected Predicate<UserVo> predicate = c -> true;

    @Override
    public boolean isValid(UserVo userVo, ConstraintValidatorContext constraintValidatorContext) {
        return predicate.test(userVo);
    }

    public static class UniqueUserValidator extends UserValidator<UniqueUser>{
        @Override
        public void initialize(UniqueUser uniqueUser) {
            UserDao userDao = ApplicationContextHolder.getBean(UserDao.class);
            predicate = c -> !userDao.existsByUserNameOrEmailOrTelphone(c.getUserName(),c.getEmail(),c.getTelphone());
        }
    }

    public static class NotConflictUserValidator extends UserValidator<NotConflictUser>{
        @Override
        public void initialize(NotConflictUser notConflictUser) {
            predicate = c -> {
                UserDao userDao = ApplicationContextHolder.getBean(UserDao.class);
                Collection<UserVo> collection = userDao.findByUserNameOrEmailOrTelphone(c.getUserName(), c.getEmail(), c.getTelphone());
                // 将用户名、邮件、电话改成与现有完全不重复的,或者只与自己重复的,就不算冲突
                return collection.isEmpty() || (collection.size() == 1 && collection.iterator().next().getId().equals(c.getId()));
            };
        }
    }

}
@Component
public class ApplicationContextHolder implements ApplicationContextAware {

    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }

    public static ApplicationContext getContext() {
        return context;
    }

    public static Object getBean(String name) {
        return context != null ? context.getBean(name) : null;
    }

    public static <T> T getBean(Class<T> clz) {
        return context != null ? context.getBean(clz) : null;
    }

    public static <T> T getBean(String name, Class<T> clz) {
        return context != null ? context.getBean(name, clz) : null;
    }

    public static void addApplicationListenerBean(String listenerBeanName) {
        if (context != null) {
            ApplicationEventMulticaster applicationEventMulticaster = (ApplicationEventMulticaster)context.getBean(ApplicationEventMulticaster.class);
            applicationEventMulticaster.addApplicationListenerBean(listenerBeanName);
        }
    }

}

3.测试

@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping("/addUser")
    public String addUser(@RequestBody @UniqueUser UserVo userVo) {
        return "addUser";
    }

    @PostMapping("/updateUser")
    public String updateUser(@RequestBody @NotConflictUser UserVo userVo) {
        return "updateUser";
    }
}

10、@Valid 和 @Validated 的区别

区别如下:

在这里插入图片描述

11、常用注解

Bean Validation 内嵌的注解很多,基本实际开发中已经够用了,注解如下:

注解详细信息
@Null任意类型。被注释的元素必须为 null
@NotNull任意类型。被注释的元素不为 null
@Min(value)数值类型(double、float 会有精度丢失)。其值必须大于等于指定的最小值
@Max(value)数值类型(double、float 会有精度丢失)。其值必须小于等于指定的最大值
@DecimalMin(value)数值类型(double、float 会有精度丢失)。其值必须大于等于指定的最小值
@DecimalMax(value)数值类型(double、float 会有精度丢失)。其值必须小于等于指定的最大值
@Size(max, min)字符串、集合、Map、数组类型。被注释的元素的大小(长度)必须在指定的范围内
@Digits (integer, fraction)数值类型、数值型字符串类型。其值必须在可接受的范围内。 integer:整数精度;fraction:小数精度
@Past日期类型。被注释的元素必须是一个过去的日期
@Future日期类型。被注释的元素必须是一个将来的日期
@Pattern(value)字符串类型。被注释的元素必须符合指定的正则表达式

Hibernate Validator 在原有的基础上也内嵌了几个注解,如下:

注解详细信息
@Email字符串类型。被注释的元素必须是电子邮箱地址
@Length字符串类型。被注释的字符串的长度必须在指定的范围内
@NotEmpty字符串、集合、Map、数组类型。 被注释的元素的长度必须非空
@Range数值类型、字符串类型。 被注释的元素必须在合适的范围内

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

剑指offer 栈习题训练经验总结(未完)

第一题 定义栈的数据结构&#xff0c;请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中&#xff0c;调用 min、push 及 pop 的时间复杂度都是 O(1)。 class MinStack {private:stack<int> s;stack<int> min_s;public:void push(int x) {s.push(x);if…

xxl-job 集成 SpringBoot 使用

文章目录 前言xxl-job 集成 SpringBoot 使用1. xxl-job 安装部署超链接2. 启动xxl-job3. 集成 SpringBoot 使用3.1. 添加依赖3.2. 执行器 配置3.3. 执行器组件配置3.4. XxlJob 开发步骤 4. &#xff08;BEAN模式&#xff09;示例5. GLUE(Java) 示例6. 注意 前言 如果您觉得有用…

视觉震撼的数据可视化示例

众所周知&#xff0c;数据可以非常强大——当你真正理解它告诉你什么时。 数据和信息可视化(数据可视化或信息可视化)是对大量复杂的定量、定性数据、信息进行设计和创建易于沟通、易于理解的图形或视觉表示的实践&#xff0c;在静态、动态或交互式视觉项目的帮助下&#xff0…

Java对象的创建方式以及对象的引用

日子就是这么的庸常&#xff0c;却有细碎的事物&#xff0c;如太阳碎碎的光芒&#xff0c;洒落其上 Java创建对象有几种方式 new创建新对象 new创建我们就很熟悉了&#xff0c;像Person p new Person();等等 通过反射机制 这种创建对象的方式就是当我们编译时不知道要创…

java面试,redis面试,java面试大全

LEARNING_CONTENT 一个分布式锁的解决方案&#xff0c;另一个是分布式事务的解决方案 -2 flink 链接&#xff1a;flink参考文章 -1 linux of view 参考链接&#xff1a; linux常见面试题 linux查看占用cup最高的10个进程的命令&#xff1b; 参考文章&#xff1a;linux查看…

uboot 启动内核代码分析

0、uboot和内核区别 uboot的本质就是一个复杂点的裸机程序。内核本身也是一个"裸机程序“&#xff0c;和uboot、和其他裸机程序并没有本质区别。 区别就是操作系统运行起来后在软件上分为内核层和应用层&#xff0c;分层后两层的权限不同&#xff0c;在内存访问和设备操作…

iptables 防火墙进出控制

iptables 防火墙进出控制 iptables简介 iptables 是集成在 Linux 内核中的包过滤防火墙系统。使用 iptables 可以添加、删除具体的过滤规则&#xff0c;iptables 默认维护着 4 个表和 5 个链&#xff0c;所有的防火墙策略规则都被分别写入这些表与链中。 “四表”是指 iptab…

ES是如何解决高可用

https://www.cnblogs.com/crazymakercircle/p/15433680.html ES是一个分布式全文检索框架&#xff0c;隐藏了复杂的处理机制&#xff0c;核心数据分片机制、集群发现、分片负载均衡请求路由。 ES的高可用架构&#xff0c;总体如下图&#xff1a; 说明&#xff1a;本文会以pdf…

SQL 招聘网站岗位数据分析

数据清洗 1.删除包含空字段的行 create view v_data_clean_null as select * from data d where job_href is not null and job_href ! and job_name is not null and job_name ! and company_href is not null and company_href ! and company_name is not null and com…

OpenAI ChatGPT Unity接入

OpenAI ChatGPT Unity接入 OpenAI ChatGPT Unity接入OpenAi-API-Unity 方法OpenAi-API-Unity 下载本地配置Unity 模块URL接入gz 接入json 接入Open AIOpenAi-Api-Unity 插件文档 OpenAi 本地化接入 Unity 方法Unity 关键字识别语音合成 & 文字转语音音频记录 & 实时音频…

基于物联网及云计算技术的智慧充电桩平台设计方案

针对目前的充电桩监管难题&#xff0c;如何逐一击破各个痛点&#xff1f; TSINGSEE可提供基于"智能充电设备&#xff0b;云平台&#xff0b;APP小程序"一体化完整的解决方案&#xff0c;解决当前充电桩运营商面临的各种运营和管理难题。 一、方案介绍 方案充分利用…

容器技术的发展

容器技术的发展 近年来&#xff0c;随着计算机硬件、网络以及云计算等技术的迅速发展&#xff0c;云原生的概念也越来越受到业界人士的广泛关注&#xff0c;越来越多的应用场景开始拥抱云原生&#xff0c;其中容器技术的发展起着至关重要的作用。本章将介绍容器技术的基础知识…

瘦身必备!四款低卡美食狂掉20斤肥肉

夏天来了&#xff0c;想要减肥瘦身&#xff0c;却总是被高卡路里的食物所困扰&#xff1f;别担心&#xff0c;今天我为大家介绍四款低卡掉秤减脂美食&#xff0c;让你轻松享受美食的同时还能达到减肥的目的。 这四款美食简单易做&#xff0c;口感也十分好吃&#xff0c;适合各…

《花雕学AI》ChatGPT 的 Prompt 用法,不是随便写就行的,这 13 种才是最有效的

ChatGPT 是一款基于 GPT-3 模型的人工智能写作工具&#xff0c;它可以根据用户的输入和要求&#xff0c;生成各种类型和风格的文本内容&#xff0c;比如文章、故事、诗歌、对话、摘要等。ChatGPT 的强大之处在于它可以灵活地适应不同的写作场景和目的&#xff0c;只要用户给出合…

【STM32】定时器PWM模式详解

PWM模式&#xff1a; PWM模式1&#xff0c;向上计数时&#xff0c;PWM信号从有效电平变为无效电平 PWM模式2&#xff0c;向上计数时&#xff0c;PWM信号从无效电平变为有效电平 PWM极性&#xff1a; 极性为高时&#xff0c;高电平为有效电平&#xff0c;低电平为无效电平 极性…

【Android取证篇】Android设备USB调试打开方式(开发者模式)

【Android取证篇】Android设备USB调试打开方式(开发者模式) Android各个版本系统手机开启”USB调试”的入口不全相同&#xff0c;仅供参考—【蘇小沐】 1、【Android1.0-3.2】 路径&#xff1a;在应用列表选择「设置」->「应用程序」->「开发」->勾选「USB调试」选…

拿来吧你——一个类帮你搞定SpringBoot中的请求日志打印

拿来吧你——一个类帮你搞定SpringBoot中的请求日志打印 日常开发工作中避免不了要打印请求日志&#xff0c;这个功能几乎在所有的项目中都需要编写一次&#xff0c;重复的次数多了&#xff0c;难免会感觉繁琐&#xff0c;因此打算搞一个通用类把这块功能拆出来。 废话不多说—…

虹科方案|使用 HK-TRUENAS支持媒体和娱乐工作流程-1

一、摘要 开发和交付能够随时随地触及受众的媒体内容变得越来越重要和复杂。 在当今高度互联、娱乐驱动的世界中&#xff0c;媒体和娱乐 (M&E) 公司需要保持竞争力才能取得成功。 这些组织需要制作各种不同格式的信息和娱乐内容&#xff0c;以便在移动设备、台式机、工作站…

MySQL---基本操作DDL(SQL特点,数据类型,对数据库的操作,对表的操作)

1. SQL的特点 具有综合统一性&#xff0c;不同数据库的支持的SQL稍有不同 非过程化语言 语言简捷&#xff0c;用户容易接受 以一种语法结构提供两种使用方式 2. 对数据库的常用操作 功能 SQL 查看所有的数据库 show databases&#xff1b; 创建数据库 create databa…

设备驱动模型:总线-设备-驱动

1 设备驱动模型简介 参考 以下内容&#xff1a; Linux 笔记&#xff1a; https://xuesong.blog.csdn.net/article/details/109522945?spm1001.2014.3001.5502正点原子-左盟主 驱动开发网络资料&#xff1a;https://www.cnblogs.com/lizhuming/category/1859545.html 1.1 概…