问题引入:填写form时应该有前端校验,后端也应该有校验
对于后端来说,外界传递的一切参数都不可靠。
前端的校验是element-ui表单验证https://element.eleme.cn/#/zh-CN/component/form
Form 组件提供了表单验证的功能,只需要通过 rules 属性传入约定的验证规则,并将 Form-Item 的 prop 属性设置为需校验的字段名即可。校验规则参见 async-validator
使用自定义校验规则可以解决字母限制的问题
var validatePass2 = (rule, value, callback) => { if (value === '') { callback(new Error('请再次输入密码')); } else if (value !== this.ruleForm.pass) { callback(new Error('两次输入密码不一致!')); } else { callback(); } }; return { rules: { checkPass: [ { validator: validatePass2, trigger: 'blur' } ],
后端:@NotNull等
1.@NotNull等步骤1:使用校验注解
在Java中提供了一系列的校验方式,它这些校验方式在“javax.validation.constraints”包中,提供了如@Email,@NotNull等注解。
里面依赖了hibernate-validator org.springframework.boot spring-boot-starter-validation
在非空处理方式上提供了@NotNull,@NotBlank和@NotEmpty
(1)@NotNull 该属性不能为null
(2)@NotEmpty 该字段不能为null或""
支持以下几种类型
1.CharSequence (length of character sequence is evaluated)字符序列(字符序列长度的计算)
2.Collection (collection size is evaluated) 集合长度的计算
3.Map (map size is evaluated) map长度的计算
4.Array (array length is evaluated) 数组长度的计算
5.上面什么意思呢?就是说如果标注的是map,它会帮你看长度
(3)@NotBlank:不能为空,不能仅为一个空格
2.@Valid内置异常这里内置异常的意思是发生异常时返回的json不是我们的R对象,而是mvc的内置类
步骤2:controller中加校验注解@Valid,开启校验,
@RequestMapping("/save") public R save(@Valid @RequestBody BrandEntity brand){ brandService.save(brand); return R.ok(); }
测试: http://localhost:88/api/product/brand/save
在postman种发送上面的请求,可以看到返回的甚至不是R对象
{ "timestamp": "2020-04-29T09:20:46.383+0000", "status": 400, "error": "Bad Request", "errors": [ { "codes": [ "NotBlank.brandEntity.name", "NotBlank.name", "NotBlank.java.lang.String", "NotBlank" ], "arguments": [ { "codes": [ "brandEntity.name", "name" ], "arguments": null, "defaultMessage": "name", "code": "name" } ], "defaultMessage": "不能为空", "objectName": "brandEntity", "field": "name", "rejectedValue": "", "bindingFailure": false, "code": "NotBlank" } ], "message": "Validation failed for object='brandEntity'. Error count: 1", "path": "/product/brand/save" }
能够看到"defaultMessage": “不能为空”,这些错误消息定义在“hibernate-validator”的“orghibernatevalidatorValidationMessages_zh_CN.properties”文件中。在该文件中定义了很多的错误规则:
javax.validation.constraints.AssertFalse.message = 只能为false javax.validation.constraints.AssertTrue.message = 只能为true javax.validation.constraints.DecimalMax.message = 必须小于或等于{value} javax.validation.constraints.DecimalMin.message = 必须大于或等于{value} javax.validation.constraints.Digits.message = 数字的值超出了允许范围(只允许在{integer}位整数和{fraction}位小数范围内) javax.validation.constraints.Email.message = 不是一个合法的电子邮件地址 javax.validation.constraints.Future.message = 需要是一个将来的时间 javax.validation.constraints.FutureOrPresent.message = 需要是一个将来或现在的时间 javax.validation.constraints.Max.message = 最大不能超过{value} javax.validation.constraints.Min.message = 最小不能小于{value} javax.validation.constraints.Negative.message = 必须是负数 javax.validation.constraints.NegativeOrZero.message = 必须是负数或零 javax.validation.constraints.NotBlank.message = 不能为空 javax.validation.constraints.NotEmpty.message = 不能为空 javax.validation.constraints.NotNull.message = 不能为null javax.validation.constraints.Null.message = 必须为null javax.validation.constraints.Past.message = 需要是一个过去的时间 javax.validation.constraints.PastOrPresent.message = 需要是一个过去或现在的时间 javax.validation.constraints.Pattern.message = 需要匹配正则表达式"{regexp}" javax.validation.constraints.Positive.message = 必须是正数 javax.validation.constraints.PositiveOrZero.message = 必须是正数或零 javax.validation.constraints.Size.message = 个数必须在{min}和{max}之间 org.hibernate.validator.constraints.CreditCardNumber.message = 不合法的xyk号码 org.hibernate.validator.constraints.Currency.message = 不合法的货币 (必须是{value}其中之一) org.hibernate.validator.constraints.EAN.message = 不合法的{type}条形码 org.hibernate.validator.constraints.Email.message = 不是一个合法的电子邮件地址 org.hibernate.validator.constraints.Length.message = 长度需要在{min}和{max}之间 org.hibernate.validator.constraints.CodePointLength.message = 长度需要在{min}和{max}之间 org.hibernate.validator.constraints.LuhnCheck.message = ${validatedValue}的校验码不合法, Luhn模10校验和不匹配 org.hibernate.validator.constraints.Mod10Check.message = ${validatedValue}的校验码不合法, 模10校验和不匹配 org.hibernate.validator.constraints.Mod11Check.message = ${validatedValue}的校验码不合法, 模11校验和不匹配 org.hibernate.validator.constraints.ModCheck.message = ${validatedValue}的校验码不合法, ${modType}校验和不匹配 org.hibernate.validator.constraints.NotBlank.message = 不能为空 org.hibernate.validator.constraints.NotEmpty.message = 不能为空 org.hibernate.validator.constraints.ParametersscriptAssert.message = 执行脚本表达式""没有返回期望结果 org.hibernate.validator.constraints.Range.message = 需要在{min}和{max}之间 org.hibernate.validator.constraints.SafeHtml.message = 可能有不安全的HTML内容 org.hibernate.validator.constraints.scriptAssert.message = 执行脚本表达式""没有返回期望结果 org.hibernate.validator.constraints.URL.message = 需要是一个合法的URL org.hibernate.validator.constraints.time.DurationMax.message = 必须小于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'} org.hibernate.validator.constraints.time.DurationMin.message = 必须大于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'}
想要自定义错误消息,可以覆盖默认的错误提示信息,如@NotBlank的默认message是
public @interface NotBlank { String message() default "{javax.validation.constraints.NotBlank.message}";
可以在添加注解的时候,修改message:
@NotBlank(message = "品牌名必须非空") private String name;
当再次发送请求时,得到的错误提示信息:
{ "timestamp": "2020-04-29T09:36:04.125+0000", "status": 400, "error": "Bad Request", "errors": [ { "codes": [ "NotBlank.brandEntity.name", "NotBlank.name", "NotBlank.java.lang.String", "NotBlank" ], "arguments": [ { "codes": [ "brandEntity.name", "name" ], "arguments": null, "defaultMessage": "name", "code": "name" } ], "defaultMessage": "品牌名必须非空", "objectName": "brandEntity", "field": "name", "rejectedValue": "", "bindingFailure": false, "code": "NotBlank" } ], "message": "Validation failed for object='brandEntity'. Error count: 1", "path": "/product/brand/save" }
但是返回的错误不是R对象,影响接收端的接收,我们可以通过局部异常处理或者统一一次处理解决
3.局部异常处理BindResult步骤3:给校验的Bean后,紧跟一个BindResult,就可以获取到校验的结果。拿到校验的结果,就可以自定义的封装。
如下两个方法是一体的
@RequestMapping("/save") public R save(@Valid @RequestBody BrandEntity brand){ brandService.save(brand); return R.ok(); } @RequestMapping("/save") public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){ // 手动处理异常 if( result.hasErrors()){ Mapmap=new HashMap<>(); //1.获取错误的校验结果 result.getFieldErrors().forEach((item)->{ //获取发生错误时的message String message = item.getDefaultMessage(); //获取发生错误的字段 String field = item.getField(); map.put(field,message); }); return R.error(400,"提交的数据不合法").put("data",map); }else { } brandService.save(brand); return R.ok();
这种是针对于该请求设置了一个内容校验,如果针对于每个请求都单独进行配置,显然不是太合适,实际上可以统一的对于异常进行处理。
4.统一异常处理@ExceptionHandler上文说到 @ ExceptionHandler 需要进行异常处理的方法必须与出错的方法在同一个Controller里面。那么当代码加入了 @ControllerAdvice,则不需要必须在同一个 controller 中了。这也是 Spring 3.2 带来的新特性。从名字上可以看出大体意思是控制器增强。 也就是说,@controlleradvice + @ ExceptionHandler 也可以实现全局的异常捕捉。
(1)抽取一个异常处理类
@ControllerAdvice标注在类上,通过“basePackages”能够说明处理哪些路径下的异常。
@ExceptionHandler(value = 异常类型.class)标注在方法上
@Slf4j @RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")//管理的controller public class GulimallExceptionControllerAdvice { @ExceptionHandler(value = Exception.class) // 也可以返回ModelAndView public R handlevalidException(MethodArgumentNotValidException exception){ Mapmap=new HashMap<>(); // 获取数据校验的错误结果 BindingResult bindingResult = exception.getBindingResult(); // 处理错误 bindingResult.getFieldErrors().forEach(fieldError -> { String message = fieldError.getDefaultMessage(); String field = fieldError.getField(); map.put(field,message); }); log.error("数据校验出现问题{},异常类型{}",exception.getMessage(),exception.getClass()); return R.error(400,"数据校验出现问题").put("data",map); } }
(2)测试: http://localhost:88/api/product/brand/save
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZirxsX9l-1642404848092)(D:%5Ccxy%5Cjava%E6%97%A5%E7%A7%AF%E6%9C%88%E7%B4%AF%5C%E6%97%A5%E7%A7%AF%E6%9C%88%E7%B4%AF.assets%5Cimage-20200429183334783.png)]
(3)默认异常处理
@ExceptionHandler(value = Throwable.class)//异常的范围更大 public R handleException(Throwable throwable){ log.error("未知异常{},异常类型{}", throwable.getMessage(), throwable.getClass()); return R.error(BizCodeEnum.UNKNOW_EXEPTION.getCode(), BizCodeEnum.UNKNOW_EXEPTION.getMsg()); }
(4)错误状态码
上面代码中,针对于错误状态码,是我们进行随意定义的,然而正规开发过程中,错误状态码有着严格的定义规则,如该在项目中我们的错误状态码定义
上面的用法主要是通过@Controller+@ExceptionHandler来进行异常拦截处理
BizCodeEnum
为了定义这些错误状态码,我们可以单独定义一个常量类(一般定义为枚举类),用来存储这些错误状态码
package com.atguigu.common.exception; public enum BizCodeEnum { UNKNOW_EXEPTION(10000,"系统未知异常"), VALID_EXCEPTION( 10001,"参数格式校验失败"); private int code; private String msg; BizCodeEnum(int code, String msg) { this.code = code; this.msg = msg; } public int getCode() { return code; } public String getMsg() { return msg; } }
(5)测试: http://localhost:88/api/product/brand/save
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fJtELzat-1642404848095)(D:%5Ccxy%5Cjava%E6%97%A5%E7%A7%AF%E6%9C%88%E7%B4%AF%5C%E6%97%A5%E7%A7%AF%E6%9C%88%E7%B4%AF.assets%5Cimage-20200429191830967.png)]
可以参考下:https://blog.csdn.net/github_36086968/article/details/103115128
补充详解:
统一异常处理:
SpringBoot中有一个@ControllerAdvice的注解,使用该注解即表示开启全局异常捕获,接下来我们只需在自定义的方法上使用@ExceptionHandler注解,并定义捕获异常的类型,那么这个自定义的方法体就是对这种类型的异常进行统一的处理。
同时还提供@RestControllerAdvice注解,就相当于@RestController。
@RestController = @Controller + @ResponseBody@RestControllerAdvice = @ControllerAdvice + @ResponseBody
统一异常处理会优先处理子类异常,也就是说当出现了空指针异常,那么会优先交给handleNullPointerException这个方法处理,如果没有明确的针对某种异常进行处理,那么handleException这个方法会自觉的处理它们。
后端无论是怎么报错,返回给前端的永远是JSON数据,之后前端就可以该跳转错误页面的跳转,该d框警告的d框!
举例:
@Data @AllArgsConstructor @NoArgsConstructor public class Resultimplements Serializable { private static final long serialVersionUID = 1L; //业务码,比如成功、失败、权限不足等 code,可自行定义 @ApiModelProperty("返回码") private int status; //返回信息,后端在进行业务处理后返回给前端一个提示信息,可自行定义 @ApiModelProperty("返回信息") private String message; //数据结果,泛型,可以是列表、单个对象、数字、布尔值等 @ApiModelProperty("返回数据") private T data; public Result(int resultCode, String message) { this.status = resultCode; this.message = message; } public class ResultGenerator { private static final String DEFAULT_SUCCESS_MESSAGE = "SUCCESS"; private static final String DEFAULT_FAIL_MESSAGE = "FAIL"; private static final int RESULT_CODE_SUCCESS = 200; private static final int RESULT_CODE_SERVER_ERROR = 500; //默认成功返回success public static Result genSuccessResult() { Result result = new Result(); result.setStatus(RESULT_CODE_SUCCESS); result.setMessage(DEFAULT_SUCCESS_MESSAGE); return result; } //自定义成功时的message public static Result genSuccessResult(String message) { Result result = new Result(); result.setStatus(RESULT_CODE_SUCCESS); result.setMessage(message); return result; } //默认成功返回success并且传入返回结果 public static Result genSuccessResult(Object data) { Result result = new Result(); result.setStatus(RESULT_CODE_SUCCESS); result.setMessage(DEFAULT_SUCCESS_MESSAGE); result.setData(data); return result; } //默认失败500状态码,不传入错误消息时默认fail,否则为传入自定义错误消息 public static Result genFailResult(String message) { Result result = new Result(); result.setStatus(RESULT_CODE_SERVER_ERROR); if (StringUtils.isEmpty(message)) { result.setMessage(DEFAULT_FAIL_MESSAGE); } else { result.setMessage(message); } return result; } //自定义错误状态码和消息 public static Result genErrorResult(int code, String message) { Result result = new Result(); result.setStatus(code); result.setMessage(message); return result; } }
@RestControllerAdvice public class MallExceptionHandler { //一般参数校验绑定异常处理 @ExceptionHandler(BindException.class) public Object bindException1(BindException e) { Result result = new Result(); result.setStatus(510); List5.分组校验功能(多场景校验)fieldErrors = e.getBindingResult().getFieldErrors(); //多个错误,取第一个 FieldError error = fieldErrors.get(0); String msg = error.getDefaultMessage(); result.setMessage(msg); return result; } //JSON参数校验绑定异常处理 @ExceptionHandler(MethodArgumentNotValidException.class) public Object bindException2(MethodArgumentNotValidException e) { Result result = new Result(); result.setStatus(510); List fieldErrors = e.getBindingResult().getFieldErrors(); //多个错误,取第一个 FieldError error = fieldErrors.get(0); String msg = error.getDefaultMessage(); result.setMessage(msg); return result; } @ExceptionHandler(Exception.class) public Object handleException(Exception e, HttpServletRequest req) { Result result = new Result(); result.setStatus(500); //区分是否为自定义异常 if (e instanceof MallException) { result.setMessage(e.getMessage()); //无效认证!请重新登录!||未登录! if (e.getMessage().equals("未登录!") || e.getMessage().equals("无效认证!请重新登录!")) { result.setStatus(416); }else if (e.getMessage().equals("管理员未登录!") || e.getMessage().equals("管理员登录过期!请重新登录!")) { result.setStatus(419); } } else if (e instanceof DuplicateKeyException){ result.setMessage("重复!"); }else { e.printStackTrace(); result.setMessage("未知异常,请联系管理员"); } return result; } }
前面解决了统一异常处理,但是现状有新的需求是对同一实体类参数也要区分场景
如果新增和修改两个接口需要验证的字段不同,比如id字段,新增可以不传递,但是修改必须传递id,我们又不可能写两个vo来满足不同的校验规则。所以就需要用到分组校验来实现。
步骤:
1.创建分组接口Insert.class Update.class
2.在VO的属性中标注@NotBlank等注解,并指定要使用的分组,如@NotNull(message = “用户姓名不能为空”,groups = {Insert.class,Update.class})
3.controller的方法上或者方法参数上写要处理的分组的接口信息,如@Validated(AddGroup.class)
1、@NotNull(groups={A.class})
1、给校验注解,标注上groups,指定什么情况下才需要进行校验
如:指定在更新和添加的时候,都需要进行校验。新增时不需要带id,修改时必须带id
在实体类的统一属性上添加多个不同的校验注解
@NotNull(message = "修改必须定制品牌id", groups = {UpdateGroup.class}) @Null(message = "新增不能指定id", groups = {AddGroup.class}) @TableId private Long brandId; @NotBlank(groups = {AddGroup.class}) @URL(message = "logo必须是一个合法的URL地址", groups={AddGroup.class, UpdateGroup.class}) private String logo; 注意上面因为@NotBlank没有指定UpdateGroup分组,所以不生效。此时update时可以不携带,但带了一定得是url地址
在这种情况下,没有指定分组的校验注解,默认是不起作用的。想要起作用就必须要加groups。
2、@Validated
业务方法参数上使用@Validated注解
@Validated的value值指定要使用的一个或多个分组
JSR-303 defines validation groups as custom annotations which an application declares for the sole purpose of using
them as type-safe group arguments, as implemented in SpringValidatorAdapter.
JSR-303 将验证组定义为自定义注释,应用程序声明的唯一目的是将它们用作类型安全组参数,如 SpringValidatorAdapter 中实现的那样。
Other SmartValidator implementations may support class arguments in other ways as well.
其他SmartValidator 实现也可以以其他方式支持类参数。
// 新增场景添加 新增分组注解 @RequestMapping("/save") public R save(@Validated(AddGroup.class) @RequestBody BrandEntity brand) { brandService.save(brand); return R.ok(); } // 删除场景添加 删除分组注解 @RequestMapping("/delete") public R delete(@RequestBody Long[] brandIds) { brandService.removeByIds(Arrays.asList(brandIds)); return R.ok(); }
总结:controller接收到之后,根据@Validated表明的分组信息,品牌对应的校验注解。
3、分组校验的默认校验
这里要是指定了分组,实体类上的注解就是指定了分组的注解才生效,
没有指定分组的默认不生效,要是没有指定分组,就是对没有指定分组的注解生效,指定分组的注解就不生效了
可以在自定义的异常分组接口中继承Default类。所有没有写明group的都属于Default分组。
此外还可以在实体类上标注@GroupSequece({A.class,B.class})指定校验顺序 通过@GroupSequence指定验证顺序:先验证A分组,如果有错误立即返回而不会验证B分组,接着如果A分组验证通过了,那么才去验证B分组,最后指定User.class表示那些没有分组的在最后。这样我们就可以实现按顺序验证分组了。 关于Default,此处我springvalidation默认生成的验证接口,验证的范围是所有带有验证信息的属性, 若是属性上方写了验证组,则是验证该组内的属性 若是验证实体类类上写了GroupSequence({}) 则说明重写了Default验证接口,Default就按照GroupSequence里所写的组信息进行验证6.自定义校验注解
Hibernate Validator提供了一系列内置的校验注解,可以满足大部分的校验需求。但是,仍然有一部分校验需要特殊定制,例如某个字段的校验,我们提供两种校验强度,当为normal强度时我们除了<>号之外,都允许出现。当为strong强度时,我们只允许出现常用汉字,数字,字母。内置的注解对此则无能为力,我们试着通过自定义校验来解决这个问题。
场景:要校验showStatus的0/1状态,可以用正则,但我们可以利用其他方式解决复杂场景。比如我们想要下面的场景
@NotNull(groups = {AddGroup.class, UpdateStatusGroup.class}) @ListValue(vals = {0,1}, groups = {AddGroup.class, UpdateGroup.class, UpdateStatusGroup.class}) private Integer showStatus;
添加依赖
javax.validation validation-api2.1.0.Final org.hibernate hibernate-validator5.4.1.Final org.glassfish javax.el3.0.1-b08
1、自定义校验注解
必须有3个属性:
message()错误信息
groups()分组校验
payload()自定义负载信息
// 自定义注解 @documented @Constraint(validatedBy = { ListValueConstraintValidator.class}) // 校验器 关联校验器和校验注解 @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) // 哪都可以标注 @Retention(RUNTIME) public @interface ListValue { // 使用该属性去Validation.properties中取 String message() default "{com.atguigu.common.valid.ListValue.message}"; Class>[] groups() default { }; Class extends Payload>[] payload() default { }; // 数组,需要用户自己指定 int[] value() default {}; }
因为上面的message值对应的最终字符串需要去ValidationMessages.properties中获得,所以我们在common中新建文件ValidationMessages.properties 文件内容: com.atguigu.common.valid.ListValue.message=必须提交指定的值 [0,1]
2、自定义校验器ConstraintValidator
上面只是定义了异常消息,但是怎么验证是否异常还没说,下面的ConstraintValidator就是说的
比如我们要限定某个属性值必须在一个给定的集合里,那么就通过重写initialize()方法,指定可以有哪些元素。
而controller接收到的数据用isValid(验证
public class ListValueConstraintValidator implements ConstraintValidator{ //<注解,校验值类型> // 存储所有可能的值 private Set set=new HashSet<>(); @Override // 初始化,你可以获取注解上的内容并进行处理 public void initialize(ListValue constraintAnnotation) { // 获取后端写好的限制 // 这个value就是ListValue里的value,我们写的注解是@ListValue(value={0,1}) int[] value = constraintAnnotation.value(); for (int i : value) { set.add(i); } } @Override // 覆写验证逻辑 public boolean isValid(Integer value, ConstraintValidatorContext context) { // 看是否在限制的值里 return set.contains(value); } }
具体的校验类需要实现ConstraintValidator接口,第一个泛型参数是所对应的校验注解类型,第二个是校验对象类型。在初始化方法initialize中,我们可以先做一些别的初始化工作,例如这里我们获取到注解上的value并保存下来,然后生成set对象。
真正的验证逻辑由isValid完成,如果传入形参的属性值在这个set里就返回true,否则返回false
3、关联校验器和校验注解
@Constraint(validatedBy = { ListValueConstraintValidator.class})
一个校验注解可以匹配多个校验器
4、使用实例
@ListValue(value = {0,1},groups ={AddGroup.class}) private Integer showStatus;
如验证手机号格式,可以参考https://blog.csdn.net/GAMEloft9/article/details/81699500
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)