本篇概述
在正常的项目开发中,我们常常需要对程序的参数进行校验来保证程序的安全性。参数校验非常简单,说白了就是对参数进行正确性验证,例如非空验证、范围验证、类型验证等等。校验的方式也有很多种。如果架构设计的比较好的话,可能我们都不需要做任何验证,或者写比较少的代码就可以满足验证的需求。如果架构设计的有缺陷,或者说压根就没有架构的话,那么我们对参数进行验证时,就需要我们写大量相对重复的代码进行验证了。
手动参数校验
下面我们还是以上一篇的内容为例,我们首先手动对参数进行校验。下面为 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 参数,分别验证为空验证及范围验证。下面我们测试一下。启动项目后,访问以下地址:
http://127.0.0.1:8080/springb…
我们看一下程序的运行结果。
因为我们没有写任何参数,所以参数验证一定是不能通过的。所以就返回的上图中的提示信息。下面我们看一下数据库中的数据,然后访问一下正确的地址,看看能不能成功的返回数据库中的数据。下图为数据库中的数据:
下面我们访问一下正确的参数,然后看一下返回的结果。访问地址:
http://127.0.0.1:8080/springb…
访问结果:
我们看上图已经成功的返回数据库中的数据了,这就是简单的参数校验,正是因为简单,所以我们就不做过多的介绍了。下面我们简单分析一下,这样做参数验证好不好。如果我们的项目比较简单,那答案一定是肯定的,因为站在软件设计角度考虑,没必要为了一个简单的功能而设计一个复杂的架构。因为越是复杂的功能,出问题的可能性就越大,程序就越不稳定。但如果站在程序开发角度,那上面的代码一定是有问题的,因为上面的代码根本没办法复用,如果要开发很多这样的项目,要进行参数验证时,那结果一定是代码中有很多相类似的代码,这显然是不合理的。那怎么办呢? 那答案就是本篇中的重点内容,也就是 SpringBoot 对参数的验证,实际上本篇的内容主要是和 Spring 内容相关和 SpringBoot 的关系不大。但 SpringBoot 中基本包括了所有 Spring 的内容,所以我们还是以 SpringBoot 项目为例。下面我们看一下,怎么在 SpringBoot 中的对参数进行校验。
ObjectError 参数校验
我们首先看一下代码,然后在详细介绍代码中的新知识。下面为接受的参数类的源码。
修改前:
package com.jilinwula.springboot.helloworld.query;
import lombok.Data;
import org.springframework.stereotype.Component;
@Component
@Data
public 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
@Data
public 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 中指定的校验方式。下面我们启动项目,来验证一下上述代码是否能成功的验证参数的正确性。我们访问下面请求地址:
http://127.0.0.1:8080/springb…
返回结果:
我们看上图成功的验证了为空的校验,下面我们试一下范围的验证。我们访问下面的请求地址:
http://127.0.0.1:8080/springb…
看一下返回结果:
我们看成功的检测到了参数范围不正确。这就是 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。下面我们看一下请求返回的结果。
我们看这回我们就获取到了验证错误的字段名子了。在实际的项目开发中,我们在返回接口数据时,大部分都会采用 json 格式的方式返回,下面我们简单封装一个返回的类,使上面的验证返回 json 格式。下面为封装的返回类的源码:
package com.jilinwula.springboot.helloworld.utils;
import lombok.Data;
@Data
public 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;
}
}
我们还是启动项目,并访问下面地址看看返回的结果:
http://127.0.0.1:8080/springb…
返回结果:
创建切面
这样我们就返回一个简单的 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
@Component
public 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;
@Data
public 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;
@Data
public 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
@RestControllerAdvice
public class UserInfoHandler {
/**
* 校验错误拦截处理
*
* @param e 错误信息集合
* @return 错误信息
*/
@ExceptionHandler(UserInfoException.class)
public Object handle(UserInfoException e) {
return e.getR();
}
}
我们在该类添加了 @RestControllerAdvice 注解。该注解就是为了定义我们统一获取异常拦截的。然后我们又添加了 @ExceptionHandler 注解,该注解就是用来拦截异常类的注解,并且可以在当前方法中,直接获取到该异常类的对象信息。这样我们直接返回这个异常类的信息就可以了。因为我们在这个自定义异常类中添加了 Return 参数,所以,我们只要反悔 Return 对象的信息即可,而不用返回整个异常的信息。下面我们访问一下下面的请求,看看上述代码是否能检测到参数不正确。请求地址:
http://127.0.0.1:8080/springb…
返回结果:
这样我们完成了参数校验的功能了,并且这种方式有很大的复用性,即使我们在写新的 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
@Data
public 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://127.0.0.1:8080/springb…
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=-1
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: 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://127.0.0.1:8080/springboot/userinfo/query?roleId=3&username=%E9%98%BF%E9%87%8C%E5%B7%B4%E5%B7%B4&id=1>
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 中的参数校验全部内容,如有不正确的欢迎留言,谢谢。
源码地址
https://github.com/jilinwula/…
原文地址
http://jilinwula.com/article/…