1. 首页
  2. >
  3. 技术专题
  4. >
  5. SpringBoot

Springboot中接口参数校验N种方法你会几个?

环境:springboot2.2.10.RELEASE

Spring Validation验证框架对参数的验证机制提供了@Validated(Spring's JSR-303规范,是标准JSR-303的一个变种),javax提供了@Valid(标准JSR-303规范),结合BindingResult对象可以直接获取错误信息。

JSR是什么?

JSR是Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。

JSR-303 是JAVA EE 6 中的一项子规范,叫做Bean Validation,Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。


关于JSR303规范中定义的注解及Hibernate的一些扩展注解可参见如下文章:

SpringMVC参数统一验证方法

在Spring环境中这两个注解都可以使用。

本篇文章主要介绍如下内容:

  • 参数校验分组
  • 单个参数校验
  • 嵌套参数校验
  • 自定义工具类参数校验
  • 国际化支持
  • AOP验证参数统一处理
  1. 校验分组
    @Validated支持分组,@Valid不支持

Bean定义:

public class Users { 	@NotEmpty(message = "姓名不能为空1", groups = G1.class) 	private String name ; 	@Min(value = 10, message = "年龄不能小于10", groups = G1.class) 	@Min(value = 20, message = "年龄不能小于20", groups = G2.class) 	private Integer age ; 	@Length(min = 6, max = 18, message = "邮箱介于6到18之间", groups = {G1.class, G2.class}) 	private String email ; 	public static interface G1 {} 	public static interface G2 {} }

这里定义了2个分组G1,G2这里的定义可以随意。

定义接口:

@CustomEndpoint @ResponseBody public class UsersController extends BaseController { 	@PackMapping(value = "/valid/save1", method = RequestMethod.POST) 	public Object save1(@RequestBody @Validated(Users.G1.class) Users user, BindingResult result) { 		Optional<List<String>> op = valid(result) ; 		if (op.isPresent()) { 			return op.get() ; 		} 		return "success" ; 	} 	 	@PackMapping(value = "/valid/save2", method = RequestMethod.POST) 	public Object save2(@RequestBody @Validated(Users.G2.class) Users user, BindingResult result) { 		Optional<List<String>> op = valid(result) ; 		if (op.isPresent()) { 			return op.get() ; 		} 		return "success" ; 	} } public class BaseController { 	protected Optional<List<String>> valid(BindingResult result) { 		if (result.hasErrors()) { 			return Optional.of(result.getAllErrors().stream().map(err -> err.getDefaultMessage()).collect(Collectors.toList())) ; 		} 		return Optional.empty() ; 	} }

这里我自定义了HandlerMapping使用了自定义的注解@CustomEndpoint和@PackMapping 具体怎么使用自定义的参见如下文章:

SpringMVC自定义注解实现接口调用

这里定义了两个接口在参数校验时指明了使用哪个分组,测试:

Springboot中接口参数校验N种方法你会几个?

Springboot中接口参数校验N种方法你会几个?

在这里我们的分组校验已经生效了。

注意:这里在做参数校验时分别指明了具体的分组,如果在Bean中没有指明具体的分组,那么这个校验将不会生效,比如这里:

@Length(min = 6, max = 18, message = "邮箱介于6到18之间") 	private String email ;

如果这里没有设定groups属性,那么这里的校验将不会生效,因为我们的Controller都指明了具体使用的分组。

  1. 单个参数校验
    JSR303的Hibernate Validator实现只能对对象进行参数校验不能对当个方法参数进行校验,Spring对此进行了扩展
@CustomEndpoint @ResponseBody @Validated public class UsersController extends BaseController { 	@PackMapping("/valid/find") 	public Object find(@NotEmpty(message = "参数Id不能为空") String id) { 		return "查询到参数【" + id + "】" ; 	} }

注意:这里在类上需要加入@Validated注解。参数的校验直接在相应的参数前写上对应的注解即可。

Springboot中接口参数校验N种方法你会几个?

同时后台抛出了异常信息:

Springboot中接口参数校验N种方法你会几个?

  1. 嵌套参数校验
    嵌套的对象需要使用@Valid 注解

情况1:

public class Users { 	@NotEmpty(message = "电话必需填写") 	private String phone ; 	@Valid 	private Address address; } public class Address { 	@NotEmpty(message = "地址信息必需填写") 	private String addr ; }

接口:

@PackMapping(value = "/valid/save3", method = RequestMethod.POST) 	public Object save3(@RequestBody @Validated Users user, BindingResult result) { 		Optional<List<String>> op = valid(result) ; 		if (op.isPresent()) { 			return op.get() ; 		} 		return "success" ; 	}

测试:

Springboot中接口参数校验N种方法你会几个?

发现地址根本就没有校验。

调整参数继续校验:

Springboot中接口参数校验N种方法你会几个?

把Address对应的属性address加上后能够校验了。

情况2:

public class Users { 	@NotEmpty(message = "电话必需填写") 	private String phone ; 	@Valid 	private Address address = new Address(); }

这里直接把Address对象new好再测试:

Springboot中接口参数校验N种方法你会几个?

这回没有添加address也可以校验了。

  1. 自定义工具类参数校验
    参数校验不一定都Controller层进行校验,也可能在Service校验,比如你一个通用的save方法,不仅你会调用,别人也会调用,那这时候就有必要在Service中进行参数的校验。
public class ValidatorUtil {  	private static Validator validator;  	static { 		validator = Validation.buildDefaultValidatorFactory().getValidator(); 	}  	/** 	 *  <p> 	 *  	抛出异常方式 	 *  </p> 	 *  <p>时间:2021年2月7日-下午5:49:29</p> 	 * @author xg 	 * @param object 校验的对象参数 	 * @param groups 分组 	 * @return void 	 */ 	public static <T> void validateParams(T object, Class<?>... groups) { 		Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups); 		if (!constraintViolations.isEmpty()) { 			String messages = constraintViolations.stream().map(cv -> cv.getMessage()).collect(Collectors.joining("\n")) ; 			throw new ParamsException(messages) ; 		} 	} 	 	/** 	 *  <p> 	 *  	收集错误提示信息 	 *  </p> 	 *  <p>时间:2021年2月7日-下午5:51:27</p> 	 * @author xg 	 * @param object 校验的对象参数 	 * @param groups 分组 	 * @return  	 * @return Optional<List<String>> 	 */ 	public static <T> Optional<List<String>> validateParamsRet(T object, Class<?>... groups) { 		Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups); 		if (!constraintViolations.isEmpty()) { 			return Optional.of(constraintViolations.stream().map(cv -> cv.getMessage()).collect(Collectors.toList())) ; 		} 		return Optional.empty() ; 	}  }

先来验证下:

public static void main(String[] args) { 		Users user = new Users() ; 		System.out.println(validateParamsRet(user).get()) ; 		validateParams(user) ; }

Springboot中接口参数校验N种方法你会几个?

控制台输出了错误信息。

工具类说明:

static { 		validator = Validation.buildDefaultValidatorFactory().getValidator(); }

这里底层是通过SPI技术来获取所有的Validator验证器:

Validation.GetValidationProviderListAction类中有如下方法:

private List<ValidationProvider<?>> loadProviders(ClassLoader classloader) { 			ServiceLoader<ValidationProvider> loader = ServiceLoader.load( ValidationProvider.class, classloader ); 			Iterator<ValidationProvider> providerIterator = loader.iterator(); 			List<ValidationProvider<?>> validationProviderList = new ArrayList<>(); 			while ( providerIterator.hasNext() ) { 				try { 					validationProviderList.add( providerIterator.next() ); 				} 				catch ( ServiceConfigurationError e ) { 				} 			} 			return validationProviderList; 		}

ServiceLoader<ValidationProvider> loader = ServiceLoader.load( ValidationProvider.class, classloader );

在我的环境中有引入Hibernate验证器。

Springboot中接口参数校验N种方法你会几个?


关于验证工厂类的获取,可以通过如下3种方式:

Springboot中接口参数校验N种方法你会几个?

  1. 国际化支持
    在resources下建立,ValidationMessages.properties文件这是默认的,中文:ValidationMessages_zh_CN.properties,英文:ValidationMessages_en_US.properties

内容如下:

name.notempty=姓名必需填写 

英文:

name.notempty=name is require

Bean配置:

public class Users { 	@NotEmpty(message = "{name.notempty}", groups = G1.class) 	private String name ; }

这里message的写法:{properties中定义的key}

Springboot中接口参数校验N种方法你会几个?

模拟英文环境:

Springboot中接口参数校验N种方法你会几个?

请求中需要添加:Accept-Language头信息指明语言。

  1. AOP验证参数统一处理
    以上都接口测试都是在每个方法中自己调用验证逻辑进行处理,接下来通过AOP类对这里有参数校验的进行统一的处理。

引入依赖:

<dependency> 			<groupId>org.aspectj</groupId> 			<artifactId>aspectjrt</artifactId> 		</dependency> 		<dependency> 			<groupId>org.aspectj</groupId> 			<artifactId>aspectjweaver</artifactId> 			<scope>runtime</scope> 		</dependency>

自定义注解:

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface EnableValidate { }

定义AOP:

@Component @Aspect public class ValidateAspect { 	 	@Pointcut("@annotation(com.pack.params.valid.EnableValidate)") 	public void valid() {}  	 	@Before("valid()") 	public void validateBefore(JoinPoint jp) { 		Object[] args = jp.getArgs() ; 		for (Object arg : args) { 			if (arg instanceof BindingResult) { 				BindingResult result = (BindingResult) arg ; 				if (result.hasErrors()) { 					String messages = result.getAllErrors().stream().map(err -> err.getDefaultMessage()).collect(Collectors.joining(",")) ; 					throw new ParamsException(messages) ; 				} 			} 		} 	} 	 }

接口:

@PackMapping(value = "/valid/save1", method = RequestMethod.POST) 	@EnableValidate 	public Object save1(@RequestBody @Validated(Users.G1.class) Users user, BindingResult result) { 		/*Optional<List<String>> op = valid(result) ; 		if (op.isPresent()) { 			return op.get() ; 		}*/ 		return "success" ; 	}

测试:

Springboot中接口参数校验N种方法你会几个?

其实在这里对于这样的异常可以用@ControllerAdvice进行全局拦截处理,统一下输出。