博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
SpringBoot参数校验
阅读量:6597 次
发布时间:2019-06-24

本文共 15448 字,大约阅读时间需要 51 分钟。

本篇概述

  在正常的项目开发中,我们常常需要对程序的参数进行校验来保证程序的安全性。参数校验非常简单,说白了就是对参数进行正确性验证,例如非空验证、范围验证、类型验证等等。校验的方式也有很多种。如果架构设计的比较好的话,可能我们都不需要做任何验证,或者写比较少的代码就可以满足验证的需求。如果架构设计的有缺陷,或者说压根就没有架构的话,那么我们对参数进行验证时,就需要我们写大量相对重复的代码进行验证了。


手动参数校验

  下面我们还是以上一篇的内容为例,我们首先手动对参数进行校验。下面为Controller源码:

package com.jilinwula.springboot.helloworld.controller;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import com.jilinwula.springboot.helloworld.query.UserInfoQuery;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.util.StringUtils;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/userinfo")public class UserInfoController {    @Autowired    private UserInfoRepository userInfoRepository;    @GetMapping("/query")    public Object list(UserInfoQuery userInfo) {        if (StringUtils.isEmpty(userInfo.getUsername())) {            return "账号不能为空";        }        if (StringUtils.isEmpty(userInfo.getRoleId()) || userInfo.getRoleId() > 100 || userInfo.getRoleId() < 1) {            return "权限不能为空,并且范围为[1-99]";        }        UserInfoEntity userInfoEntity = userInfoRepository.findByUsernameAndRoleId(userInfo.getUsername(), userInfo.getRoleId());        return userInfoEntity;    }}

  我们只验证了username和roleId参数,分别验证为空验证及范围验证。下面我们测试一下。启动项目后,访问以下地址:

  我们看一下程序的运行结果。

  title

  因为我们没有写任何参数,所以参数验证一定是不能通过的。所以就返回的上图中的提示信息。下面我们看一下数据库中的数据,然后访问一下正确的地址,看看能不能成功的返回数据库中的数据。下图为数据库中的数据:

  title

  下面我们访问一下正确的参数,然后看一下返回的结果。访问地址:

  访问结果:

  title

  我们看上图已经成功的返回数据库中的数据了,这就是简单的参数校验,正是因为简单,所以我们就不做过多的介绍了。下面我们简单分析一下,这样做参数验证好不好。如果我们的项目比较简单,那答案一定是肯定的,因为站在软件设计角度考虑,没必要为了一个简单的功能而设计一个复杂的架构。因为越是复杂的功能,出问题的可能性就越大,程序就越不稳定。但如果站在程序开发角度,那上面的代码一定是有问题的,因为上面的代码根本没办法复用,如果要开发很多这样的项目,要进行参数验证时,那结果一定是代码中有很多相类似的代码,这显然是不合理的。那怎么办呢?那答案就是本篇中的重点内容,也就是SpringBoot对参数的验证,实际上本篇的内容主要是和Spring内容相关和SpringBoot的关系不大。但SpringBoot中基本包括了所有Spring的内容,所以我们还是以SpringBoot项目为例。下面我们看一下,怎么在SpringBoot中的对参数进行校验。


ObjectError参数校验

  我们首先看一下代码,然后在详细介绍代码中的新知识。下面为接受的参数类的源码。

  修改前:

package com.jilinwula.springboot.helloworld.query;import lombok.Data;import org.springframework.stereotype.Component;@Component@Datapublic class UserInfoQuery{    private String username;    private Long roleId;}

  修改后:

package com.jilinwula.springboot.helloworld.query;import lombok.Data;import org.springframework.stereotype.Component;import javax.validation.constraints.Max;import javax.validation.constraints.Min;import javax.validation.constraints.NotNull;@Component@Datapublic class UserInfoQuery{    @NotNull(message = "账号不能为空")    private String username;    @NotNull(message = "权限不能为空")    @Min(value = 1, message = "权限范围为[1-99]")    @Max(value = 99, message = "权限范围为[1-99]")    private Long roleId;}

  我们看代码中唯一的区别就是添加了很多的注解。没错,在SpringBoot项目中进行参数校验时,就是使用这些注解来完成的。并且注解的命名很直观,基本上通过名字就可以知道什么含义。唯一需要注意的就是这些注解的包是javax中的,而不是其它第三方引入的包。这一点要特别注意,因为很多第三方的包,也包含这些同名的注解。下面我们继续看Controller中的改动(备注:有关javax中的校验注解相关的使用说明,我们后续在做介绍)。Controller源码:

  改动前:

package com.jilinwula.springboot.helloworld.controller;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import com.jilinwula.springboot.helloworld.query.UserInfoQuery;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.util.StringUtils;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/userinfo")public class UserInfoController {    @Autowired    private UserInfoRepository userInfoRepository;    @GetMapping("/query")    public Object list(UserInfoQuery userInfo) {        if (StringUtils.isEmpty(userInfo.getUsername())) {            return "账号不能为空";        }        if (StringUtils.isEmpty(userInfo.getRoleId()) || userInfo.getRoleId() > 100 || userInfo.getRoleId() < 1) {            return "权限不能为空,并且范围为[1-99]";        }        UserInfoEntity userInfoEntity = userInfoRepository.findByUsernameAndRoleId(userInfo.getUsername(), userInfo.getRoleId());        return userInfoEntity;    }}

  改动后:

package com.jilinwula.springboot.helloworld.controller;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import com.jilinwula.springboot.helloworld.query.UserInfoQuery;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.validation.BindingResult;import org.springframework.validation.ObjectError;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import javax.validation.Valid;@RestController@RequestMapping("/userinfo")public class UserInfoController {    @Autowired    private UserInfoRepository userInfoRepository;    @GetMapping("/query")    public Object list(@Valid UserInfoQuery userInfo, BindingResult result) {        if (result.hasErrors()) {            for (ObjectError error : result.getAllErrors()) {                return error.getDefaultMessage();            }        }        UserInfoEntity userInfoEntity = userInfoRepository.findByUsernameAndRoleId(userInfo.getUsername(), userInfo.getRoleId());        return userInfoEntity;    }}

  我们看代码改动的还是比较大的首先在入参中添加了@Valid注解。该注解就是标识让SpringBoot对请求参数进行验证。也就是和参数类里的注解是对应的。其次我们修改了直接在Controller中进行参数判断的逻辑,将以前的代码修改成了SpringBoot中指定的校验方式。下面我们启动项目,来验证一下上述代码是否能成功的验证参数的正确性。我们访问下面请求地址:

  返回结果:

  title

  我们看上图成功的验证了为空的校验,下面我们试一下范围的验证。我们访问下面的请求地址:

  看一下返回结果:

  title

  我们看成功的检测到了参数范围不正确。这就是SpringBoot中的参数验证功能。但上面的代码一个问题,就是只是会返回错误的提示信息,而没有提示,是哪个参数不正确。下面我们修改一下代码,来看一下怎么返回是哪个参数不正确。

FieldError参数校验

package com.jilinwula.springboot.helloworld.controller;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import com.jilinwula.springboot.helloworld.query.UserInfoQuery;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.validation.BindingResult;import org.springframework.validation.FieldError;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import javax.validation.Valid;@RestController@RequestMapping("/userinfo")public class UserInfoController {    @Autowired    private UserInfoRepository userInfoRepository;    @GetMapping("/query")    public Object list(@Valid UserInfoQuery userInfo, BindingResult result) {        if (result.hasErrors()) {            FieldError error = result.getFieldError();            return error.getField() + "+" + error.getDefaultMessage();        }        UserInfoEntity userInfoEntity = userInfoRepository.findByUsernameAndRoleId(userInfo.getUsername(), userInfo.getRoleId());        return userInfoEntity;    }}

  我们将获取ObjectError的类型修改成了FieldError。因为FieldError类型可以获取到验证错误的字段名字,所以我们将ObjectError修改为FieldError。下面我们看一下请求返回的结果。

  title

  我们看这回我们就获取到了验证错误的字段名子了。在实际的项目开发中,我们在返回接口数据时,大部分都会采用json格式的方式返回,下面我们简单封装一个返回的类,使上面的验证返回json格式。下面为封装的返回类的源码:

package com.jilinwula.springboot.helloworld.utils;import lombok.Data;@Datapublic class Return {    private int code;    private Object data;    private String msg;    public static Return error(Object data, String msg) {        Return r = new Return();        r.setCode(-1);        r.setData(data);        r.setMsg(msg);        return r;    }}

  Controller修改:

package com.jilinwula.springboot.helloworld.controller;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import com.jilinwula.springboot.helloworld.query.UserInfoQuery;import com.jilinwula.springboot.helloworld.utils.Return;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.validation.BindingResult;import org.springframework.validation.FieldError;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import javax.validation.Valid;@RestController@RequestMapping("/userinfo")public class UserInfoController {    @Autowired    private UserInfoRepository userInfoRepository;    @GetMapping("/query")    public Object list(@Valid UserInfoQuery userInfo, BindingResult result) {        if (result.hasErrors()) {            FieldError error = result.getFieldError();            return Return.error(error.getField(), error.getDefaultMessage());        }        UserInfoEntity userInfoEntity = userInfoRepository.findByUsernameAndRoleId(userInfo.getUsername(), userInfo.getRoleId());        return userInfoEntity;    }}

  我们还是启动项目,并访问下面地址看看返回的结果:

  返回结果:

  title

创建切面

  这样我们就返回一个简单的json类型的数据了。虽然我们的校验参数的逻辑没有在Controller里面写,但我们还是在Controller里面写了很多和业务无关的代码,并且这些代码还是重复的,这显然是不合理的。我们可以将上述相同的代码的封装起来,然后统一的处理。这样就避免了有很多重复的代码了。那这代码封装到哪里呢?我们可以使用Spring中的切面功能。因为SpringBoot中基本包括了所有Spring中的技术,所以,我们可以放心大胆的在SpringBoot项目中使用Spring中的技术。我们知道在使用切面技术时,我们可以对方法进行前置增强、后置增强、环绕增强等。这样我们就可以利用切面的技术,在方法之前,也就是请求Controller之前,做参数的校验工作,这样就不会对我们的业务代码产生侵入了。下面我们看一下切面的源码然后在做详细说明:

package com.jilinwula.springboot.helloworld.aspect;import com.jilinwula.springboot.helloworld.utils.Return;import lombok.extern.slf4j.Slf4j;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.springframework.stereotype.Component;import org.springframework.validation.BindingResult;import org.springframework.validation.FieldError;@Slf4j@Aspect@Componentpublic class UserAspect {    @Before("execution(public * com.jilinwula.springboot.helloworld.controller..*(..))")    public void doBefore(JoinPoint joinPoint) {        for (Object arg : joinPoint.getArgs()) {            if (arg instanceof BindingResult) {                BindingResult result = (BindingResult) arg;                if (result.hasErrors()) {                    FieldError error = result.getFieldError();                    Return.error(error.getField(), error.getDefaultMessage());                }            }        }    }}

  我们看上述的代码中我们添加了一个@Aspect注解,这个就是切面的注解,然后我们在方法中又添加了@Before注解,也就是对目标方法进行前置增强,Spring在请求Controller之前会先请求此方法。所以我们可以将校验参数的代码逻辑写在这个方法中。execution参数为切点函数,也就是目标方法的切入点。切点函数包含一些通配符的语法,下面我们简单介绍一下:

    • 匹配任意字符,但它可能匹配上下文中的一个元素
  • .. 匹配任意字符,可以匹配上下文中的多个元素
    • 表示按类型匹配指定类的所有类,必须跟在类名后面,也就是会匹配继承或者扩展指定类的所有类,包括指定类.

创建异常类

  我们通过上述代码知道,Spring中的切面功能是没有返回值的。所以我们在使用切面功能时,是没有办法在切面里面做参数返回的。那我们应该怎么办呢?这时异常就派上用场了。我们知道当程序抛出异常时,如果当前方法没有做try catch处理,那么异常就会一直向上抛出,如果程序也一直没有做处理,那么当前异常就会一直抛出,直到被Java虚拟机捕获。但Java虚拟机也不会对异常进行处理,而是直接抛出异常。这也就是程序不做任何处理抛出异常的根本原因。我们正好可以利用异常的这种特性,返回参数验证的结果。因为在Spring中为我们提供了统一捕获异常的方法,我们可以在这个方法中,将我们的异常信息封装成json格式,这样我们就可以返回统一的jons格式了。所以在上述的切面中我们手动了抛出了一个异常。该异常因为我们没有用任何处理,所以上述异常会被SpringBoot中的统一异常拦截处理。这样当SpringBoot检测到参数不正确时,就会抛出一个异常,然后SpringBoot就会检测到程序抛出的异常,然后返回异常中的信息。下面我们看一下异常类的源码:

  异常类:

package com.jilinwula.springboot.helloworld.exception;import com.jilinwula.springboot.helloworld.utils.Return;import lombok.Data;@Datapublic class UserInfoException extends RuntimeException {    private Return r;    public UserInfoException(Return r) {        this.r = r;    }}

  Return源码:

package com.jilinwula.springboot.helloworld.utils;import com.jilinwula.springboot.helloworld.exception.UserInfoException;import lombok.Data;@Datapublic class Return {    private int code;    private Object data;    private String msg;    public static void error(Object data, String msg) {        Return r = new Return();        r.setCode(-1);        r.setData(data);        r.setMsg(msg);        throw new UserInfoException(r);    }    public static Return success() {        Return r = new Return();        r.setCode(0);        return r;    }}

SpringBoot统一异常拦截

  因为该异常类比较简单,我们就不会过多的介绍了,唯一有一点需要注意的是该异常类继承的是RuntimeException异常类,而不是Exception异常类,原因我们已经在上一篇中介绍了,Spring只会回滚RuntimeException异常类及其子类,而不会回滚Exception异常类的。下面我们看一下Spring中统一拦截异常处理,下面为该类的源码:

package com.jilinwula.springboot.helloworld.handler;import com.jilinwula.springboot.helloworld.exception.UserInfoException;import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.RestControllerAdvice;@Slf4j@RestControllerAdvicepublic class UserInfoHandler {    /**     * 校验错误拦截处理     *     * @param e 错误信息集合     * @return 错误信息     */    @ExceptionHandler(UserInfoException.class)    public Object handle(UserInfoException e) {        return e.getR();    }}

  我们在该类添加了@RestControllerAdvice注解。该注解就是为了定义我们统一获取异常拦截的。然后我们又添加了@ExceptionHandler注解,该注解就是用来拦截异常类的注解,并且可以在当前方法中,直接获取到该异常类的对象信息。这样我们直接返回这个异常类的信息就可以了。因为我们在这个自定义异常类中添加了Return参数,所以,我们只要反悔Return对象的信息即可,而不用返回整个异常的信息。下面我们访问一下下面的请求,看看上述代码是否能检测到参数不正确。请求地址:

  返回结果:

  title

  这样我们完成了参数校验的功能了,并且这种方式有很大的复用性,即使我们在写新的Controller,也不需要手动的校验参数了,只要我们的请求参数是UserInfoQuery类就可以了。还有一点要注意,所以我们不用手动验证参数了,但我们的请求参数中还是要写BindingResult参数,这一点要特别注意。


正则表达式校验注解

  下面我们更详细的介绍一下参数验证的注解,我们首先看一下正则校验,我们在实体类中添加一个新属性,然后用正则的的方式,验证该参数的正确性。下面为实体类源码:

package com.jilinwula.springboot.helloworld.query;import lombok.Data;import org.springframework.stereotype.Component;import javax.validation.constraints.Max;import javax.validation.constraints.Min;import javax.validation.constraints.NotNull;import javax.validation.constraints.Pattern;@Component@Datapublic class UserInfoQuery{    @NotNull(message = "用户编号不能为空")    @Pattern(regexp = "^[1-10]$",message = "用户编号范围不正确")    private String id;    @NotNull(message = "账号不能为空")    private String username;    @NotNull(message = "权限不能为空")    @Min(value = 1, message = "权限范围为[1-99]")    @Max(value = 99, message = "权限范围为[1-99]")    private Long roleId;}

  下面我们访问以下地址:

http文件请求接口

  但这回我们不在浏览器里请求,因为浏览器请求不太方便,并且返回的json格式也没有格式化不方便浏览,除非要装一些浏览器插件才可以。实际上在IDEA中我们可以很方便的请求一下接口地址,并且返回的json内容是自动格式化的。下面我们来看一下怎么在IDEA中发起接口请求。在IDEA中请求一个接口很简单,我们只要创建一个.http类型的文件名字就可以。然后我们可以在该文件中,指定我们接口的请求类型,例如GET或者POST。当我们在文件的开口写GET或者POST时,IDEA会自动有相应的提示。下面我们看一下http文件中的内容。

  http.http:

GET http://127.0.0.1:8080/springboot/userinfo/query?roleId=3&username=阿里巴巴&id=-1

  这时标识GET参数的地方,就会出现绿色剪头,但我们点击这个绿色箭头,IDEA就会就会启动请求GET参数后面的接口。下面我们看一下上述的返回结果。

GET http://127.0.0.1:8080/springboot/userinfo/query?roleId=3&username=%E9%98%BF%E9%87%8C%E5%B7%B4%E5%B7%B4&id=-1HTTP/1.1 200 Content-Type: application/json;charset=UTF-8Transfer-Encoding: chunkedDate: Mon, 18 Feb 2019 03:57:29 GMT{  "code": -1,  "data": "id",  "msg": "用户编号范围不正确"}Response code: 200; Time: 24ms; Content length: 41 bytes

  这就是.http文件类型的返回结果,用该文件请求接口,相比用浏览器来说,要方便的多。因为我们在实体类中使用正则指定参数范围为1-10,所以请求接口时反悔了id参数有错误。下面我们输入一个正确的值在看一下返回结果。

  http.http:

GET http://127.0.0.1:8080/springboot/userinfo/query?roleId=3&username=阿里巴巴&id=1

  返回结果:

  GET 
  HTTP/1.1 200  Content-Type: application/json;charset=UTF-8  Transfer-Encoding: chunked  Date: Mon, 18 Feb 2019 05:46:49 GMT  {  "id": 61,  "username": "阿里巴巴",  "password": "alibaba",  "nickname": "阿里巴巴",  "roleId": 3  }  Response code: 200; Time: 25ms; Content length: 77 bytes

常见校验注解

  我们看已经正确的返回数据库中的数据了。在Spring中,提供了很多种注解来方便我们进行参数校验,下面是比较常见的注解:

注解 作用
@Null 参数必须为null
@NotNull 参数必须不为null
@NotBlank 参数必须不为null,并且长度必须大于0
@NotEmpty 参数必须不为空
@Min 参数必须大于等于该值
@Max 参数必须小于等于该值
@Size 参数必须在指定的范围内
@Past 参数必须是一个过期的时间
@Future 参数必须是一个未来的时间
@Pattern 参数必须满足正则表达式
@Email 参数必须为电子邮箱

  上述内容就是SpringBoot中的参数校验全部内容,如有不正确的欢迎留言,谢谢。


源码地址

原文地址

转载地址:http://tvvio.baihongyu.com/

你可能感兴趣的文章
比较常用的几个正则表达式
查看>>
linux svn服务器搭建、客户端操作、备份与恢复
查看>>
IIS默认网站无法开启service unavailable!
查看>>
知识点②:spring boot 注入 / 静态注入
查看>>
Catalyst2层交换的3层通信
查看>>
Struts2中OGNL对各种方法的调用
查看>>
JavaScript强化教程——javascript性能优化
查看>>
输入框自动下拉补全
查看>>
Non-terminating decimal expansion; no exact re...
查看>>
raspberry pi和 oLinuxIno (same as Openboard Swo...
查看>>
mysql explain中的select tables optimized away
查看>>
Elasticsearch 查询
查看>>
hadoop和hbase整合
查看>>
备份网络配置文件的几个建议
查看>>
python Thread already started 错误解决
查看>>
一个故事讲完https
查看>>
关于PHP和英语共同点的思考
查看>>
排序 —— 归并排序
查看>>
Xstream 简单实用教程
查看>>
Nagios监控之11:NRPE脚本监测信息输出格式
查看>>