Java中的参数校验

Posted 攻城狮Chova

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java中的参数校验相关的知识,希望对你有一定的参考价值。

参数校验规则

需要进行参数校验

  • 对外提供的开放接口. 无论是RPC,API还是HTTP接口
  • 敏感权限入口
  • 需要极高稳定性可用性的方法
  • 调用频次低的方法
  • 执行开销很大的方法:
    • 参数校验的时间可以忽略不计
    • 如果因为参数错误会导致中间执行被退回或者错误时代价很大

不需要进行参数校验

  • 可能被循环调用的方法不需要进行参数校验,但是需要在方法说明中注明外部参数的检查要求
  • 声明为private的方法,只会被本身类的代码调用:
    • 如果已经确定调用的方法代码传入参数已经做过检查或者肯定不会有问题,则不需要进行参数校验
  • 底层调用频度比较高的方法

JSR 303

基本概念

  • JSR: Java Specification Requests.Java规范提案,指的是向JCP,也就是Java Community Process提出新增一个标准化技术规范的正式请求.任何人都可以提交JSR,来向Java平台新增API和服务
  • JSR 303:
    • JavaEE 6中的一项子规范,叫作Bean Validation
    • Hibernate ValidatiorBean Validation的参考实现
      • Hibernate Validator提供JSR 303规范中所有内置约束constraint的实现,而且还有一些附加的约束constraint
    • SpringBoot中内置了Hibernate Validator
  • JSR 303的作用:
    • 用于对Java Bean的字段值进行校验,确保输入的数据在语义上的正确性,使得验证逻辑和业务代码相分离
    • JSR 303是运行时数据验证框架,验证后的错误信息会立即返回

基本使用

  • if校验:
@Data
public class User 
	/**
	 * 用户名
	 */
	private String name;

@PostMapping("/user")
public Result addUser(@RequestBody User user) 
	// 参数校验
	if (StringUtils.isEmpty(user.getName())) 
		throw new RuntimeException("用户姓名必须提交!");
	
	return Result.buildSuccess(user);

  • 使用if判断可以完成校验,但是如果遇到有很多属性需要校验的场景时,程序中就必须要有很多if判断,这样就会造成代码冗余.此时可以使用JSR 303完成校验

@NotBlank

  • @NotBlank: 非空校验
    • 在需要校验的属性添加 @NotBlank注解
    @Data
    public class User 
      
      @NotBlank(message = "用户姓名必须提交!")
      private String name;
    
    
  • 在方法的入参参数添加 @Valid注解
    @PostMapping("/user")
    public Result addUser(@Valid @RequestBody User user) 
    	return Result.buildSuccess(user);
    
    

统一异常处理

  • 从输出结果上可以看到 @NotBlank中的message信息,对于错误的输出结果 ,Spring中提供了对应的类BindingResult来存储错误信息,为方法添加BindingResult参数:
@PostMapping("/user")
public Result addUser(@Valid @RequestBody User user, BindingResult result) 
	if (result.hasErrors()) 
		Map<String, String> map = new HashMap<>();
		result.getFieldErrors().forEach(item -> 
			// 获取错误提示信息message
			String message = item.getDefaultMessage();
			// 获取发生错误的字段
			String field = item.getField();
			map.put(field, message);
			System.out.println(field + ":" + message); 
		); 
		return Result.buildFailure(400, "参数错误", map);
	
	return Result.buildSuccess(user);

  • Spring提供的MethodArgumentNotValidException类中,包含BindingResult的引用,因此BindingResult类可以获取到输出的错误信息
public class MethodArgumentNotValidException extends Exception 
	private final MethodParameter parameter;
	private final BindingResult bindingResult;

	public MethodArgumentNotValidException(MethodParameter parameter, BindingResult bindingResult) 
		this.parameter = parameter;
		this.bindingResult = bindingResult;
	

	public MethodParameter getParameter() 
		return this.parameter;
	

	public BindingResult getBindingResult() 
		return this.bindingResult;
	

  • 统一异常处理代码:
@Slf4j
@RestControllerAdvice(annotations = RestController.class, Controller.class)
public class GlobalExceptionHandler 

	@ResponseStatus(HttpStatus.OK)
	@ExceptionHandler(MethodArgumentNotValidException.class)
	public Result handValidationException(MethodArgumentNotValidException e) 
		log.error(ErrorStatus.ILLEGAL_DATA.getMessage() + ":" + e.getMessage());
		Map<String, Object> map = new HashMap<>();
		 
		// 获取异常输出信息
		BindingResult bindingResult = e.getBindingResult();
		bindingResult.getFieldErrors().forEach(fieldError -> 
			String message = fieldError.getDefaultMessage();
			String field = fieldError.getField();
			map.put(field, message);
		);
		return Result.buildFailure(ErrorStatus.ILLEGAL_DATA.getCode(), String.valueOf(getFirstOrNull(map)), map);
		

	private static Object getFirstOrNull(Map<String, Object> map) 
		Object obj = null;
		for (Map.Entry<String, Object> entry : map.entrySet()) 
			obj = entry.getValue();
			if (null != obj) 
				break;
			
		
		return obj;
	

JSR 303注解

@Valid和@Validated

@Valid和@Validated比较

  • 相同点:
    • @Valid注解和 @Validated注解都是开启校验功能的注解
  • 不同点:
    • @Validated注解是Spring基于 @Valid注解的进一步封装,并提供比如分组,分组顺序的高级功能
    • 使用位置不同:
      • @Valid注解 : 可以使用在方法,构造函数,方法参数和成员属性上
      • @Validated注解 : 可以用在类型,方法和方法参数上. 但是不能用在成员属性上

@Valid高级使用

@Valid级联校验

  • 级联校验: 也叫嵌套检测.嵌套就是一个实体类包含另一个实体类
  • @Valid和可以用在成员属性的字段上,因此 @Valid可以提供级联校验
  • 示例:
    @Data
    public class Hair 
    	
    	@NotBlank(message = "头发长度必须提交!")
    	private Double length;
    
      	@NotBlank(message = "头发颜色必须提交!")
      	private String color;
    
    
    @Data
    public class Person 
    	
    	@NotBlank(message = "用户姓名必须提交!")
    	@Size(min=2, max=8)
    	private String userName;
    
      	// 添加@Valid注解实现嵌套检测
      	@Valid
        @NotEmpty(message = "用户要有头发!")
        private List<Hair> hairs;
    
     
    @PostMapping("/person")
    public Result addPerson(@Valid @RequestBody Person person) 
    	return Result.buildSuccess(person);
    
    
    • 只是在方法参数前面添加 @Valid@Validated注解,不会对嵌套的实体类进行校验.要想实现对嵌套的实体类进行校验,需要在嵌套的实体类属性上添加 @Valid注解

@Validated高级使用

@Validated分组校验

  • 分组校验:
    • 对指定的组开启校验,可以分别作用于不同的业务场景中
    • 分组校验是由 @Validated注解中的value提供的
  • groups:
    • JSR 303校验注解中的分组方法groups
    • 示例:
    @Data
    public class PersonGroup 
    	
    	public interface AddGroup 
      
      	public interface UpdateGroup 
    
      	// @Validated注解value方法指定分组UpdateGroup.class时校验
      	@NotBlank(message = "用户ID必须提交!", groups = UpdateGroup.class)
      	private String id;
    
      	// @Validated注解value方法指定分组AddGroup.class或者分组UpdateGroup.class时校验
      	@NotBlank(message = "用户的姓名必须提交!", groups = AddGroup.class, UpdateGroup.class) 
      	private String name;
    
      	// @Validated注解value方法未指定分组时校验
      	@Range(min = 1, max = 200, message = "用户的年龄必须提交!")
      	private int age;
    
    
  • 开启分组校验: 通过 @Validated注解的value方法对指定的分组开启校验
@RestController
@RequestMapping("/person")
public class PersonGroupController 
	
	// 不指定分组时校验
	@GetMapping("/person")
	public Result getPerson(@Validated @RequestBody PersonGroup person) 
		return Result.buildSuccess(person);
	

	// 指定AddGroup分组校验
	@PostMapping("/person")
	public Result addPerson(@Validated(value = PersonGroup.AddGroup.class) @RequestBody PersonGroup person) 
		return Result.buildSuccess(person);
	

	// 指定UpdateGroup分组校验
	@PutMapping("/person")
	public Result updatePerson(@Validated(value = PersonGroup.updateGroup.class) @RequestBody PersonGroup person) 
		return Result.buildSuccess(person);
	

  • 校验方法添加groups的值来指定分组,只有使用 @Validated注解的value的值指定这个分组时,开会开启注解的校验数据的功能

@Validated分组校验顺序

  • 默认情况下,分组间的约束是无序的,但是在一些特殊的情况下可能对分组间的校验有一定的顺序
    • 比如第二组的分组的约束的校验需要依赖第一组的稳定状态来进行,此时,要求分组间的约束校验一定要有顺序
  • 分组校验顺序通过使用 @GroupSequence注解实现
  • 示例:
@Data
public class UserGroupSequence 
	
	public interface FirstGroup 

	public interface SecondGroup 

	// 使用GroupSequence定义分组校验顺序:按照FirstGroup,SecondGroup分组顺序进行校验
	@GroupSequence(FirstGroup.class, SecondGroup.class)
	public interface Group 

	@NotEmpty(message = "用户ID必须提交!", group = FirstGroup.class)
	private String userId;

	@NotEmpty(message = "用户姓名必须提交!", group = FirstGroup.class)
	@Size(min = 2, max = 8, message = "用户姓名的长度在2~8之间", goup = Second.class)
	private String userName;
 
@RestController
@RequestMapping("/user")
public class UserGroupSequenceController 
	// 这里方法中@Validated注解value的值是Group.class
	@PostMapping("/user")
	public Result addGroup(@Validated(value = Group.class) @RequestBody UserGroupSequence user) 
		return Result.buildSuccess(user);
	

  • 使用 @GroupSequence注解指定分组校验顺序后,第一组分组的约束的校验没有通过后,就不会进行第二组分组的约束的校验

@Validated非实体类校验

  • 在非实体类上添加 @Validated注解对非实体类进行校验
@Validated
public class AnnotationController 
	
	@GetMapping("/person")
	public Result getAge(@Range(min = 2, max = 8, message = "年龄在3~8岁!") @RequestParam int age) 
		return Result.buildSuccess(age);
	

  • GlobalExceptionHandler中添加全局统一异常处理方法:
@ExceptionHandler(ConstraintViolationException.class)
@ResponseBody
public Result resolveConstraintViolationException(ConstraintVilationException exception) 
	Set<ConstraintVilation<?>> constraintVilations = exception.getConstraintVilations();
	// 处理异常信息
	if (!CollectionUtils.isEmpty(constraintVilations)) 
		StringBuilder messageBuilder = new StringBuilder();
		for (ConstraintVilation constraintViolation : constraintVilations) 
			messageBuilder.append(constraintVilation.getMessage()).append(",");
		
		String errorMessage = messageBuilder.toString();
		if (errorMessage.length() > 1) 
			errorMessage.substring(0, errorMessage.length() - 1);
		
		return Result.builderFailure(ErrorStatus.ILLEGAL_DATA.getCode(), errorMessage);
	 
	return Result.builderFailure(ErrorStatus.ILLEGAL_DATA.getCode(), exception.getMessage())

@PathVariable

  • @PathVariable的作用: 用来指定请求URL路径里面的变量
  • @PathVariable@RequestParam的区别:
    • @PathVariable用来指定请求URL中的变量
    • @RequestParam用来获取静态的URL请求入参

正则表达式校验

  • 使用正则表达式校验 @PathVariable指定的路径变量
// 请求路径中的id必须是数字,否则寻找不到这个路径404
@GetMapping("/user/id:\\\\d+")
public Result getId(@PathVariable(name="id") String userId) 
	return Result.buildSuccess(userId);

继承BasicErrorController类

  • @ControllerAdvice注解只能处理进入控制器方法抛出的异常
  • BasicErrorController接口可以处理全局异常
  • @PathVariable路径校验异常不是控制器方法抛出的,此时还没有进入控制器方法:
    • BasicErrorController处理异常,比如404异常时,会跳转到 /error路径,此时会返回错误的html页面
    • 为了保证返回结果统一,继承BasicErrorController类,重写BasicErrorController接口中的错误处理方法
@RestController
public class PathErrorController extends BasicErrorController 
	
	@Autowired
	public PathErrorController(ErrorAttributes errorAttributes, ServerProperties serverProperties, List<ErrorViewResolver> errorViewResolvers) 
		super(errorAttributes, serverProperties.getError(), errorViewResolvers);
	

	/**
	 * 处理html请求
	 */
	@Override
	public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) 
		HttpStatus status = getStatus(request);
		Map<String, Object> model = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML));
		ModelAndView modelAndView = new ModelAndView("pathErrorPage", model, status);
		return modelAndView;
	
	
	/**
	 * 处理json请求
	 */
	@Override
	public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) 
		Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
		
		Map<String, Object> responseBody = new HashMap<>(8);
		responseBody.put("success", false);
		responseBody.put("code", body.get("status"));
		responseBody.put("message", body.get("error")); 
		
		return new ResponseEntity<>(responseBody, HttpStatus.OK);
	
 

自定义校验注解

  • 使用场景:
    • 对某一个只能输入指定值的字段进行校验. 此时需要使用自定义注解实现
  • 定义自定义的注解 @Show :
@Documented
@Constraint(validateBy = Show.ShowConstraintValidator.class)
@Target以上是关于Java中的参数校验的主要内容,如果未能解决你的问题,请参考以下文章

Java参数验证Bean Validation 框架

Struts2请求参数校验

Java参数校验工具validation实践

片段(Java) | 机试题+算法思路+考点+代码解析 2023

ASP.net MVC 代码片段问题中的 Jqgrid 实现

Java Bean Validation 最佳实践