Spring 参数校验最佳实践及原理解析

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 背景在参数校验框架出现前,业务逻辑代码中经常会充斥着参数校验的代码,将参数校验视为业务逻辑的一部分不失为是一种较为灵活的做法,然而对于一些通用的校验,如字符串不能为空、数值范围限制等,如果还放在业务逻辑中,则会导致业务代码出现大量的重复,为了避免这种问题,Java 社区提出了 JSR-303 规范,用于对 bean 进行校验。

背景


在参数校验框架出现前,业务逻辑代码中经常会充斥着参数校验的代码,将参数校验视为业务逻辑的一部分不失为是一种较为灵活的做法,然而对于一些通用的校验,如字符串不能为空、数值范围限制等,如果还放在业务逻辑中,则会导致业务代码出现大量的重复,为了避免这种问题,Java 社区提出了 JSR-303 规范,用于对 bean 进行校验。


Spring 框架横空出世后,它又提出了一套更为简单易用的校验接口,校验作为 Spring 的核心特性之一,能够和 Spring 其他组件有机地整合到一起。


应用场景


Spring 内部主要将校验应用于 WEB 环境下参数绑定后的校验,并预留给用户一些接口用于常规校验。


常规参数校验


Spring 参数校验作为 spring-context 模块的一部分存在,Validator 是 Spring 参数校验的核心接口,我们先看下这个接口。


public interface Validator {
  // 当前验证器是否支持给定的类型
  boolean supports(Class<?> clazz);
  // 校验给定的 target,提供的 errors 对象用于存储和获取错误信息
  void validate(Object target, Errors errors);
}


接口中只存在两个方法,supports 方法用于校验前确认是否支持给定的类型,validate方法用于参数校验,比较奇怪的是校验结果使用参数中的 Errors 存储及获取。Validator 的类图如下。


image.png


Spring 未提供单独的 Validator 实现,而是将 JSR-303 中的 Validator 与 Spring Validator 整合到一起,SpringValidatorAdpter 作为 Spring Validator 与 JSR-303 Validator 的适配器,底层使用 JSR-303 作为实现,除此之外 使用最多的是 LocalValidatorFactoryBean,这个类可以配置创建 JSR-303 Validator 的参数。关于 JSR-303 java bean validation,你还可以参考《Java Bean Validation 详解》这篇文章,内容相对比较详实。


Errors 同样是一个相对重要的概念,其类图如下。


5.png


比较常用的 Errors 实现是 BeanPropertyBindingResult,这个类不仅表示用于表示校验结果,还表示数据绑定到对象的属性上的结果。


有了上述 Spring 校验相关的基础知识后,我们就可以在应用中使用 Spring Validator。首先需要引入 JSR-303 规范的实现,这里我们引入的是 hibernate-validator。


        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.0.22.Final</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>javax.el</artifactId>
            <version>3.0.1-b09</version>
        </dependency>


示例代码如下。


@Data
public class LoginDTO {
    @NotBlank(message = "用户名不能为空")
    private String username;
    @NotBlank(message = "密码不能为空")
    private String password;
}
public class App {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(App.class);
        Validator validator = context.getBean(LocalValidatorFactoryBean.class);
        LoginDTO loginDTO = new LoginDTO();
        Errors errors = new BeanPropertyBindingResult(loginDTO, "login");
        validator.validate(loginDTO, errors);
        errors.getAllErrors().forEach(System.out::println);
        context.close();
    }
    @Bean
    public LocalValidatorFactoryBean localValidatorFactoryBean() {
        return new LocalValidatorFactoryBean();
    }
}


打印结果如下。


Field error in object 'login' on field 'password': rejected value [null]; codes [NotBlank.login.password,NotBlank.password,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [login.password,password]; arguments []; default message [password]]; default message [密码不能为空]
Field error in object 'login' on field 'username': rejected value [null]; codes [NotBlank.login.username,NotBlank.username,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [login.username,username]; arguments []; default message [username]]; default message [用户名不能为空]


可以看到,成功将校验的结果存到了 Errors 中,如果你使用的是 spring-boot,还可以直接引入 spring-boot-starter-validation,Spring 会自动引入相关依赖并注册 LocalValidatorFactoryBean 作为 bean,此时可以直接把 Validator 注入 bean 中。


web 环境参数校验


web 环境下,Spring 会从 request 中获取相关数据,然后绑定到 conroller 方法的参数上。有关校验的参数,具体可以分为三类。


简单类型的参数


Spring 从 request 中根据参数名获取数据然后设置到参数中,支持的注解包括 @CookieValue、@MatrixVariable、@PathVariable、@RequestAttribute、@RequestHeader、@RequestParam、@SessionAttribute。


对于这类参数,可以设置注解的 required 参数来配置参数是否必须(默认必须),如果参数必须且未传参数则会抛出 ServletRequestBindingException 异常。示例代码如下。


@RestController
@RequestMapping("/user")
public class UserController {
    @PostMapping("/login")
    Result<LoginBO> login(@RequestParam(required = true) String username, @RequestParam(required = true) String password) {
        return null;
    }
}


那如果想对方法参数进行其他校验怎么办呢?Spring 内部还提供了一个 MethodValidationPostProcessor,这个处理器会利用 AOP 特性拦截标注了 @Validated 注解的 bean 的方法的执行,在目标方法执行前后执行校验。因此,将 MethodValidationPostProcessor 注册为 bean,然后在目标方法的参数上添加校验注解即可。


spring-boot 环境下可直接引入 spring-boot-starter-validation ,这个依赖同样会自动注册 MethodValidationPostProcessor 作为 bean。需要注意的是如果校验不通过抛出的是 JSR-303 规范中定义的 ConstraintViolationException 异常。


示例代码如下。


@Validated
@RestController
@RequestMapping("/user")
public class UserController {
    @PostMapping("/login")
    Result<LoginBO> login(@NotBlank String username, @NotBlank String password) {
        return null;
    }
}


非简单类型的表单参数


对于 get 请求或 x-www-form-urlencoded 类型的 post 请求,可以把单个简单类型的参数存入一个普通的 Java 对象中,将这个 Java 对象作为 controller 方法的参数,Spring 会自动从请求中获取参数信息然后实例化这个类并作为参数值传入方法中。


对于这类参数,在类的字段上添加校验相关注解,然后在 controller 方法参数上添加 @Validated 或 @Valid 或以 @Valid 开头命名的注解才会开启注解功能,此时如果校验不通过将会抛出 BindException 异常。校验的示例代码如下。


@Data
public class LoginDTO {
    @NotBlank(message = "用户名不能为空")
    private String username;
    @NotBlank(message = "密码不能为空")
    private String password;
}
@RestController
@RequestMapping("/user")
public class UserController {
    @PostMapping("/login")
    Result<LoginBO> login(@Validated LoginDTO dto) {
        return null;
    }
}


如果不想抛出异常,可以将要校验的方法参数的后一个参数设置为 Errors 类型或其子类型,由这个 Errors 接受参数校验结果,由于方法参数和数据绑定有关,通常我们可以设置为 BindingResult。示例代码如下。


@RestController
@RequestMapping("/user")
public class UserController {
    @PostMapping("/login")
    Result<LoginBO> login(@Validated LoginDTO dto, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            List<String> errorMessageList = bindingResult.getAllErrors().stream()
                    .map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.toList());
        }
        return null;
    }
}


非简单类型的其他请求体参数


除了表单类型,目前我们使用最多的请求内容类型是 application/json,校验方式和表单类型一样,在参数前添加 @Validated 开启校验,后一个参数使用 Errors 类型可以接收校验结果。与表单类型校验不用的是,如果后一个参数不是 Errors 类型,抛出的异常是 MethodArgumentNotValidException。示例代码如下。


@RestController
@RequestMapping("/user")
public class UserController {
    @PostMapping("/login")
    Result<LoginBO> login(@Validated @RequestBody LoginDTO dto) {
        return null;
    }
}


参数校验全局异常处理


手动使用 Validator 进行参数校验,或者在处理器方法参数中配置 Errors 类型的参数都可以直接取到校验结果,然而这种写法仍然和业务代码耦合在一起,虽然比较灵活,但是大多数情况如果校验不通过我们直接返回失败原因即可,因此对于这种通用的情况,我们可以定义一个全局的异常处理器,捕获 Spring 校验抛出的异常,并将异常转换为错误消息返回到前端。


@RestControllerAdvice
public class GlobalExceptionHandler {
    // 处理标注了 @Validated 的类的方法调用参数校验失败导致的异常
    @ExceptionHandler(ConstraintViolationException.class)
    public Result<?> handleConstraintViolationException(ConstraintViolationException e) {
        String message = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining("|"));
        return Result.fail(message);
    }
    // 处理表单类型请求普通参数缺失导致的异常
    @ExceptionHandler(ServletRequestBindingException.class)
    public Result<?> handleServletRequestBindingException(ServletRequestBindingException e) {
        String message = e.getMessage();
        return Result.fail(message);
    }
    // 处理表单类型请求的复杂参数校验失败导致的异常
    @ExceptionHandler(BindException.class)
    public Result<?> handleBindException(BindException e) {
        String message = e.getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining("|"));
        return Result.fail(message);
    }
    // 处理 application/json 类型请求的参数校验失败导致的异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining("|"));
        return Result.fail(message);
    }
}


Spring Web 环境 Controller 方法参数校验原理解析


Spring 允许 Controller 处理器方法上定义请求相关的不同参数,Spring 在调用处理器方法时需要收集参数值,为此,Spring 定义了一个 HandlerMethodArgumentResolver 接口用于根据参数元数据解析出参数值,解析参数值时同时也会对参数值进行校验,这个接口最终会被 DispatchServlet 间接调用。


先看 HandlerMethodArgumentResolver 的接口定义。


public interface HandlerMethodArgumentResolver {
  // 是否支持参数
  boolean supportsParameter(MethodParameter parameter);
  // 解析参数值
  Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
               NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}


接口比较简单,只有两个方法,如果支持给定的方法参数,则会解析参数值,像常见的 @RequestParam、@RequestBody 就是由不同的实现处理的。以处理 @RequestBody 注解参数的 RequestResponseBodyMethodProcessor 为例,其参数校验的核心代码如下。


public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
  @Override
  public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
                  NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
    parameter = parameter.nestedIfOptional();
    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);
  }
}
public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {
  // 参数校验
  protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
    Annotation[] annotations = parameter.getParameterAnnotations();
    for (Annotation ann : annotations) {
      Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
      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;
      }
    }
  }
}


从这里可以看到 Spring 先解析参数值,解析后进行参数校验,并且还可以看出 Spring 会把 @Validated 的 value 参数值作为校验分组,而 @Valid 注解则无法指定分组,这也是 @Validated 和 @Valid 在 Spring 中的不同之处。


总结

本篇主要介绍了 Spring 使用 Validator 的各种方式,包括手动调用 Validator 接口方法校验、通过 MethodValidationPostProcessor 拦截目标对象方法执行校验、以及 web 环境下 Spring 内部自动进行的数据绑定和参数校验。数据绑定除了应用在 web 环境的处理器方法参数,还会应用到 Spring 内部的其他地方,是 Spring 的核心特性之一,下篇进行分析。


目录
相关文章
|
11天前
|
负载均衡 算法 Java
Spring Cloud全解析:负载均衡算法
本文介绍了负载均衡的两种方式:集中式负载均衡和进程内负载均衡,以及常见的负载均衡算法,包括轮询、随机、源地址哈希、加权轮询、加权随机和最小连接数等方法,帮助读者更好地理解和应用负载均衡技术。
|
6天前
|
存储 缓存 Java
什么是线程池?从底层源码入手,深度解析线程池的工作原理
本文从底层源码入手,深度解析ThreadPoolExecutor底层源码,包括其核心字段、内部类和重要方法,另外对Executors工具类下的四种自带线程池源码进行解释。 阅读本文后,可以对线程池的工作原理、七大参数、生命周期、拒绝策略等内容拥有更深入的认识。
什么是线程池?从底层源码入手,深度解析线程池的工作原理
|
7天前
|
机器学习/深度学习 Java API
阿里云文档智能解析——大模型版能力最佳实践与体验评测
阿里云文档智能解析(大模型版)在处理非结构化数据方面表现优异,尤其是在性能和可扩展性上具有明显优势。虽然存在一些待完善之处,但其强大的基础能力和广泛的适用场景使其成为企业数字转型过程中的有力助手。随着技术的不断进步和完善,相信它会在更多领域展现出更大的价值。
27 5
阿里云文档智能解析——大模型版能力最佳实践与体验评测
|
9天前
|
XML 监控 Java
Spring Cloud全解析:熔断之Hystrix简介
Hystrix 是由 Netflix 开源的延迟和容错库,用于提高分布式系统的弹性。它通过断路器模式、资源隔离、服务降级及限流等机制防止服务雪崩。Hystrix 基于命令模式,通过 `HystrixCommand` 封装对外部依赖的调用逻辑。断路器能在依赖服务故障时快速返回备选响应,避免长时间等待。此外,Hystrix 还提供了监控功能,能够实时监控运行指标和配置变化。依赖管理方面,可通过 `@EnableHystrix` 启用 Hystrix 支持,并配置全局或局部的降级策略。结合 Feign 可实现客户端的服务降级。
75 23
|
7天前
|
人工智能 自然语言处理 监控
文档解析(大模型版)能力最佳实践测评
文档解析(大模型版)能力最佳实践测评
28 7
|
17天前
|
域名解析 网络协议
DNS服务工作原理
文章详细介绍了DNS服务的工作原理,包括FQDN的概念、名称解析过程、DNS域名分级策略、根服务器的作用、DNS解析流程中的递归查询和迭代查询,以及为何有时基于IP能访问而基于域名不能访问的原因。
37 2
|
20天前
|
持续交付 jenkins Devops
WPF与DevOps的完美邂逅:从Jenkins配置到自动化部署,全流程解析持续集成与持续交付的最佳实践
【8月更文挑战第31天】WPF与DevOps的结合开启了软件生命周期管理的新篇章。通过Jenkins等CI/CD工具,实现从代码提交到自动构建、测试及部署的全流程自动化。本文详细介绍了如何配置Jenkins来管理WPF项目的构建任务,确保每次代码提交都能触发自动化流程,提升开发效率和代码质量。这一方法不仅简化了开发流程,还加强了团队协作,是WPF开发者拥抱DevOps文化的理想指南。
39 1
|
13天前
|
负载均衡 网络协议 安全
DNS解析中的Anycast技术:原理与优势
【9月更文挑战第7天】在互联网体系中,域名系统(DNS)将域名转换为IP地址,但网络规模的扩张使DNS解析面临高效、稳定与安全挑战。Anycast技术应运而生,通过将同一IP地址分配给多个地理分布的服务器,并依据网络状况自动选择最近且负载低的服务器响应查询请求,提升了DNS解析速度与效率,实现负载均衡,缓解DDoS攻击,增强系统高可用性。此技术利用动态路由协议如BGP实现,未来在网络发展中将扮演重要角色。
40 0
|
20天前
|
开发者 图形学 API
从零起步,深度揭秘:运用Unity引擎及网络编程技术,一步步搭建属于你的实时多人在线对战游戏平台——详尽指南与实战代码解析,带你轻松掌握网络化游戏开发的核心要领与最佳实践路径
【8月更文挑战第31天】构建实时多人对战平台是技术与创意的结合。本文使用成熟的Unity游戏开发引擎,从零开始指导读者搭建简单的实时对战平台。内容涵盖网络架构设计、Unity网络API应用及客户端与服务器通信。首先,创建新项目并选择适合多人游戏的模板,使用推荐的网络传输层。接着,定义基本玩法,如2D多人射击游戏,创建角色预制件并添加Rigidbody2D组件。然后,引入网络身份组件以同步对象状态。通过示例代码展示玩家控制逻辑,包括移动和发射子弹功能。最后,设置服务器端逻辑,处理客户端连接和断开。本文帮助读者掌握构建Unity多人对战平台的核心知识,为进一步开发打下基础。
42 0
|
20天前
|
存储 C# 关系型数据库
“云端融合:WPF应用无缝对接Azure与AWS——从Blob存储到RDS数据库,全面解析跨平台云服务集成的最佳实践”
【8月更文挑战第31天】本文探讨了如何将Windows Presentation Foundation(WPF)应用与Microsoft Azure和Amazon Web Services(AWS)两大主流云平台无缝集成。通过具体示例代码展示了如何利用Azure Blob Storage存储非结构化数据、Azure Cosmos DB进行分布式数据库操作;同时介绍了如何借助Amazon S3实现大规模数据存储及通过Amazon RDS简化数据库管理。这不仅提升了WPF应用的可扩展性和可用性,还降低了基础设施成本。
42 0

热门文章

最新文章

推荐镜像

更多