随手记录8:@Validated统一参数校验失败统一异常处理
简介
项目中比如controller层代码入口处请求参数校验内容,可以统一做成对应的内容;而不是在业务代码中对参数进行一一判定.重复冗余大量代码内容,可以直接通过org.springframework.validation.annotation.Validated 注解来完成参数的校验. 当然校验参数的并不只有controller, 还有可能从dubbo 接口请求过来;或者我们自定义的拦截里面的参数校验–>可以使用jorg.hibernate.validator.HibernateValidator 来完成参数的校验
如果是controller层的请求参数还可以通过统一的异常处理类完成校验异常信息内容的收集操作
demo下载
项目结构内容
还是基于前几篇文章内容的项目只是修改里面的业务代码内容还是基于springboot 项目的
代码内容
1.接受请求参数的实体类
可以看到UserReq中嵌套的有OrderReq 如果orderReq中的请求参数也需要校验则需要在属性上添加@Valid注解 完成嵌套对象中的属性校验.
package com.khy.req;
import java.util.Date;
import javax.validation.Valid;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.Range;
public class UserReq {
private Long userId;
@Range(message = "年龄范围为 {min} 到 {max} 之间", min = 1, max = 100)
private Integer age;
@NotBlank(message = "用户名不能为空")
private String userName;
@NotBlank(message = "密码不能为空")
private String password;
@NotNull
@Valid //重点!!!
private OrderReq orderReq;
private Date birthday;
public OrderReq getOrderReq() {
return orderReq;
}
public void setOrderReq(OrderReq orderReq) {
this.orderReq = orderReq;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public Integer getAge() {
return age;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public void setAge(Integer age) {
this.age = age;
}
}
2.在controller方法中完成异常信息的拼装校验返回的操作
下面是两种传递参数的方式在对应的方法里面完成参数校验但是很不方便—>
/**
* 这个是在方法对校验的返回的异常信息进行操作
* @author khy
* @createTime 2021年5月18日下午5:38:34
* @param userReq
* @param bindingResult
* @return
*/
@RequestMapping("/saveUser1")
public String saveUser(@Validated @RequestBody UserReq userReq,BindingResult bindingResult){
if(bindingResult.hasErrors()){
JSONObject jsonObject = new JSONObject();
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
List<String>errorList = fieldErrors.stream().map(e->e.getDefaultMessage()).collect(Collectors.toList());
Map<String, String> errorMap = fieldErrors.stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
jsonObject.put("errorList", errorList);
jsonObject.put("errorMap", errorMap);
System.out.println("saveUser1 方法中请求参数校验未通过"+jsonObject.toJSONString());
}
return "saveUser1 执行完毕";
}
@RequestMapping("/getUser1")
public String getUser(@Validated UserReq userReq,BindingResult bindingResult){
if(bindingResult.hasErrors()){
JSONObject jsonObject = new JSONObject();
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
List<String>errorList = fieldErrors.stream().map(e->e.getDefaultMessage()).collect(Collectors.toList());
Map<String, String> errorMap = fieldErrors.stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
jsonObject.put("errorList", errorList);
jsonObject.put("errorMap", errorMap);
System.out.println("getUser1 方法中请求参数校验未通过"+jsonObject.toJSONString());
}
return "getUser1 执行完毕";
}
3.优化上面的方法统一异常处理中完成参数的校验;
/**
* @Validated 注解使用 ;
* 如果不做任何处理则会直接将所有校验异常信息全部返回
* 这个是传递的json格式的请求参数
* 如果发现请求参数中有不符合条件的属性配置则直接抛出
* org.springframework.web.bind.MethodArgumentNotValidException
* @author khy
* @createTime 2021年5月18日下午4:17:26
* @param userReq
* @return
*/
@RequestMapping("/saveUser")
public String saveUser(@Validated @RequestBody UserReq userReq){
System.out.println(JSON.toJSONString(userReq));
return "saveUser 执行完毕";
}
/**
* 这个是普通的表单参数传递使用的方式
* 参数不符合条件则抛出
* org.springframework.validation.BindException 异常信息
* @author khy
* @createTime 2021年5月18日下午5:35:23
* @param userReq
* @return
*/
@RequestMapping("/getUser")
public String getUser(@Validated UserReq userReq){
System.out.println(JSON.toJSONString(userReq));
return "saveUser 执行完毕";
}
两种传递参数 都是通过@Validated 注解完成参数校验;但是由于传递参数不同;所以导致了校验不通过抛出的 异常不一样所以在通用的异常处理类中拦截的异常以及处理的逻辑也是不一样的
4.统一异常处理类
@RestController
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 请求参数校验传递的json格式参数校验
* @author khy
* @createTime 2021年5月18日下午5:24:55
* @param req
* @param exception
* @return
* @throws Exception
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
@ResponseBody
public Object validatedPostException(HttpServletRequest req, MethodArgumentNotValidException exception) {
JSONObject json = new JSONObject();
json.put("path", req.getRequestURI());
BindingResult bindingResult = exception.getBindingResult();
if(bindingResult.hasErrors()){
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
List<String>errorList = fieldErrors.stream().map(e->e.getDefaultMessage()).collect(Collectors.toList());
Map<String, String> errorMap = fieldErrors.stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
json.put("errorList", errorList);
json.put("errorMap", errorMap);
}
json.put("timestamp", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
return json;
}
/**
* 普通的参数传递的形式;
* @author khy
* @createTime 2021年5月18日下午5:25:16
* @param req
* @param exception
* @return
* @throws Exception
*/
@ExceptionHandler(value = BindException.class)
@ResponseBody
public Object validatedGetException(HttpServletRequest req, BindException exception){
JSONObject json = new JSONObject();
json.put("path", req.getRequestURI());
if(exception.hasErrors()){
List<FieldError> fieldErrors = exception.getFieldErrors();
List<String>errorList = fieldErrors.stream().map(e->e.getDefaultMessage()).collect(Collectors.toList());
Map<String, String> errorMap = fieldErrors.stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
json.put("errorList", errorList);
json.put("errorMap", errorMap);
}
json.put("timestamp", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
return json;
}
@ExceptionHandler(value = Exception.class)
@ResponseBody
public Object exception(HttpServletRequest req, Exception e) throws Exception {
JSONObject json = new JSONObject();
json.put("retCode", "003");
json.put("retMes",e.getMessage());
json.put("path", req.getRequestURI());
json.put("timestamp", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
return json;
}
}
案例效果内容
1. 传递json格式的参数信息直接抛出异常在全局异常处理类中进行异常处理
请求参数信息以及响应信息内容
请求代码中的方法以及统一异常处理类中的代码逻辑
2. 传递的表单参数 (抛出的异常和上面的不一样)
传递的是表单数据 展示请求参数和响应信息内容
分组操作
项目中 同一个请求参数可能在不同的入口处理的校验条件不一样;这时间就需要对接受请求参数的实体类中的属性内容进行分组操作;简单理解就是不同的接口要求校验的参数也是不一样的.
案例中定义interface FirstGroup/SecondGroup 两个分组信息;
下面请求参数修改成带有分组的
1.分组请求的req
package com.khy.req;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.NotEmpty;
import org.hibernate.validator.constraints.Range;
/***
* 处理分组
* @author khy
* @createTime 2021年5月19日上午10:08:31
*/
public class GroupReq {
@NotBlank(message = "用户名不能为空")
private String name;
@NotNull(groups={FirstGroup.class},message="password不能为空")
private String password;
@Range(groups={FirstGroup.class},message = "年龄范围为 {min} 到 {max} 之间", min =18, max = 20)
@Min(groups={SecondGroup.class},message = "年龄不能低于18",value=18)
private Integer age;
@NotBlank(message="邮箱不能为空")
// @Email(message="邮箱不合法")
private String email;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
2.分组请求的controller入口代码校验
@RequestMapping("/group")
public class GroupController {
@RequestMapping("/defaultGroup")
public String defaultGroup(@Validated(value={Default.class}) @RequestBody GroupReq groupReq,HttpServletRequest request){
Locale locale = request.getLocale();
System.out.println(JSON.toJSONString(groupReq));
return "defaultGroup 执行完毕"+JSON.toJSONString(groupReq);
}
@RequestMapping("/firstGroup")
public String firstGroup(@Validated(value={FirstGroup.class}) @RequestBody GroupReq groupReq){
System.out.println(JSON.toJSONString(groupReq));
return "firstGroup 执行完毕"+JSON.toJSONString(groupReq);
}
@RequestMapping("/secondGroup")
public String SecondGroup(@Validated(value={SecondGroup.class}) @RequestBody GroupReq groupReq){
System.out.println(JSON.toJSONString(groupReq));
return "secondGroup 执行完毕"+JSON.toJSONString(groupReq);
}
}
案例效果
1.测试默认分组的案例
该方法@Validated 不指定/指定javax.validation.groups.Default 走默认分组的校验
校验通过的 因为默认分组只校验了name
2.测试FristGroup
3.测试SecondGroup
自定义方法里面的校验
项目的入口可能还有dubbo接口/或者我们指定的方法做参数校都可以使用
org.hibernate.validator.HibernateValidator
package com.khy.utils;
import org.hibernate.validator.HibernateValidator;
import com.alibaba.fastjson.JSON;
import com.khy.req.FirstGroup;
import com.khy.req.GroupReq;
import com.khy.req.SecondGroup;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 自定义的拦截类内容
* @author khy
* @createTime 2021年5月19日下午6:04:46
*/
public class ValidationUtil {
public static void main(String[] args) {
GroupReq groupReq = new GroupReq();
// groupReq.setName("candy");
// groupReq.setEmail("ssss");
ValidResult validateBean = validateBean(groupReq);
System.out.println(JSON.toJSONString(validateBean));
groupReq.setAge(17);
validateBean = validateBean(groupReq,FirstGroup.class);
System.out.println(JSON.toJSONString(validateBean));
validateBean = validateBean(groupReq,SecondGroup.class);
System.out.println(JSON.toJSONString(validateBean));
}
/**
* 开启快速结束模式 failFast (true)
*/
private static Validator validator = Validation.byProvider(HibernateValidator.class).configure().failFast(false).buildValidatorFactory().getValidator();
/**
* 校验对象
*
* @param t bean
* @param groups 校验组
* @return ValidResult
*/
public static <T> ValidResult validateBean(T t, Class<?>... groups) {
ValidResult result = new ValidResult();
Set<ConstraintViolation<T>> violationSet = validator.validate(t, groups);
boolean hasError = violationSet != null && violationSet.size() > 0;
result.setHasErrors(hasError);
if (hasError) {
for (ConstraintViolation<T> violation : violationSet) {
result.addError(violation.getPropertyPath().toString(), violation.getMessage());
}
}
return result;
}
/**
* 校验bean的某一个属性
*
* @param obj bean
* @param propertyName 属性名称
* @return ValidResult
*/
public static <T> ValidResult validateProperty(T obj, String propertyName) {
ValidResult result = new ValidResult();
Set<ConstraintViolation<T>> violationSet = validator.validateProperty(obj, propertyName);
boolean hasError = violationSet != null && violationSet.size() > 0;
result.setHasErrors(hasError);
if (hasError) {
for (ConstraintViolation<T> violation : violationSet) {
result.addError(propertyName, violation.getMessage());
}
}
return result;
}
/**
* 校验结果类
*/
public static class ValidResult {
/**
* 是否有错误
*/
private boolean hasErrors;
/**
* 错误信息
*/
private List<ErrorMessage> errors;
public ValidResult() {
this.errors = new ArrayList<>();
}
public boolean hasErrors() {
return hasErrors;
}
public void setHasErrors(boolean hasErrors) {
this.hasErrors = hasErrors;
}
/**
* 获取所有验证信息
*
* @return 集合形式
*/
public List<ErrorMessage> getAllErrors() {
return errors;
}
/**
* 获取所有验证信息
*
* @return 字符串形式
*/
public String getErrors() {
StringBuilder sb = new StringBuilder();
for (ErrorMessage error : errors) {
sb.append(error.getPropertyPath()).append(":").append(error.getMessage()).append(" ");
}
return sb.toString();
}
public void addError(String propertyName, String message) {
this.errors.add(new ErrorMessage(propertyName, message));
}
/**
* 获取去重之后的非法属性值,以逗号分隔
* @return
*/
public String getProperties() {
return errors.stream().map(error -> error.getPropertyPath()).collect(Collectors.toSet()).stream().collect(Collectors.joining(", "));
}
public boolean isHasErrors() {
return hasErrors;
}
public void setErrors(List<ErrorMessage> errors) {
this.errors = errors;
}
}
public static class ErrorMessage {
private String propertyPath;
private String message;
public ErrorMessage() {
}
public ErrorMessage(String propertyPath, String message) {
this.propertyPath = propertyPath;
this.message = message;
}
public String getPropertyPath() {
return propertyPath;
}
public void setPropertyPath(String propertyPath) {
this.propertyPath = propertyPath;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
}
注意一点内容
自定义校验本地方法的案例内容–也可以指定对应的分组内容完成分组参数的校验.