up:: SpringBoot电商项目商品分类模块增加目录分类接口
说明:
(1) 本篇博客的逻辑:
● 为什么写本篇博客?: 在【SpringBoot电商项目商品分类模块增加目录分类接口】中,Controller处接收参数的时候,当我们通过AddCategoryReq这个bean接收到参数后,我们需要校验下参数是否为空;在这篇博客中,我们采用笨笨的方式去校验;
显然,在多时候我们都需要进行参数校验;比如上面的非空检验等;而,为了能够更加优雅的实现参数校验,于是就引出了本篇博客介绍的@Valid注解;
● 但是,在使用@Valid注解时候,引入的一个新问题: 但是,在使用@Valid参数校验时候,如果校验失败,其引发的异常【MethodArgumentNotValidException异常】;但是,我们在GlobalExceptionHandler中并没有特别处理这种异常(而是用Exception这种粗粒度的方式,去处理了),所以就出现了返回信息太宽泛,没有反映出具体错误信息的情况;
● 解决策略(这也是本篇博客重难点): 在GlobalExceptionHandler编写对应的方法去处理【MethodArgumentNotValidException异常】,以能根据【MethodArgumentNotValidException异常的具体错误信息】去构建对应的APIRestResponse统一返回对象 ;(这其中涉及到了如【getBindingResult()】、【BindingResult.hasErrors()】、【BindingResult.getAllErrors()】、【objectError.getDefaultMessage()】等与Validation校验异常相关的方法)
一:参数校验的注解:【@Valid】,【@NotNull,@Max(value),@Size(max,min)】:简述;
说明:
(1) 如果我们在Controller处,给某个入参添加上@Valid注解,就表示这个入参需要校验;@Valid注解就像一个开关一样,用了这个注解就表示开启校验,不用这个注解就表示不用校验;
(2) @NotNULL,@Max(value),@Size(max,min)表示了校验规则;可以根据校验需求,在对应的属性上,使用这些注解;
(3) 单纯看这些描述,可能云里雾里;具体,看下下面的案例;
二:参数校验的注解:【@Valid】,【@NotNULL,@Max(value),@Size(max,min)】:案例演示;
1.【@Valid】,【@NotNULL,@Max(value),@Size(max,min)】:使用案例;
说明:
(1) Spring Boot2.3版本将不再内部依赖validator;所以对于2.3以后的版本,如果还想使用@Valid参数校验,需要自己先手动引入validator依赖。
2.启动项目,实测;以及引出新的问题(参数校验引发的【MethodArgumentNotValidException异常】没有去单独处理);
(1)测试结果:参数校验生效了;
看下控制台的异常日志信息:
日志信息,很明确:
至此,说明参数校验已经生效了;
(2)仍存问题:系统异常处理粒度太大:没有去单独处理参数校验引发的【MethodArgumentNotValidException异常】;
但是,还有个问题:上面出现了错误后,详细的提示信息只出现在了日志中,我们程序员是可以看到的;但是,我们这个商城的用户却不能知道具体的错误原因,商城的用户只能得到“系统异常”这个笼统的错误信息;
可以看到,上面抛出的异常是【MethodArgumentNotValidException】;而,这个异常是是继承自Exception的;
而,我们在GlobalExceptionHandler中处理异常的时候;针对异常的处理,太粗略了:
即,如果异常是Exception或者Exception的子异常,但又不是ImoocMallException异常的话;最终的处理结果都是返回【return ApiRestResponse.error(ImoocMallExceptionEnum.SYSTEM_ERROR);】;也就是说,如果系统抛出了【MethodArgumentNotValidException这个继承自Exception的异常】时,是handleException()这个方法处理的;
显然,对于这种处理方式:返回的ApiRestResponse统一返回对象中,错误的信息就是【SYSTEM_ERROR(20001,“系统异常”);】;没有,反映出【MethodArgumentNotValidException异常】的具体信息([个数必须在2和5之间]);
这也就导致了接口的返回结果,只是【系统异常】,不理想;
三:在【GlobalExceptionHandler】中,去处理【MethodArgumentNotValidException异常】:根据该异常的具体信息,去构建ApiRestResponse统一返回对象;
在GlobalExceptionHandler中,编写【handleMethodArgumentNotValidException()方法】和【handleBindingResult()方法】去处理 【MethodArgumentNotValidException异常】;
1.思路梳理;
● 前面的ImoocMallException异常,是我们自己写的,我们是根据接口的返回信息构建的;所以,通过ImoocMallException异常的信息,我们可以快速构建ApiRestResponse统一返回对象:
● 我们也创建了一个兜底的方法,去拦截Exception异常;然后,目前针对这个异常,我们就用【ImoocMallExceptionEnum.SYSTEM_ERROR:系统异常】去构建ApiRestResponse统一返回对象;
显然,对于【那些是Exception,或者继承自Exception,但不是ImoocMallException】的异常,在构建ApiRestResponse统一返回对象时,都用【SYSTEM_ERROR:系统异常】太粗放了;
我们希望在参数校验,抛出【MethodArgumentNotValidException异常】时候,能获取该异常的具体信息,然后根据这个具体信息,构建对应的ApiRestResponse统一返回对象;
● 所以,我们在GlobalExceptionHandler中,编写方法,主要内容是:去获取【MethodArgumentNotValidException】异常的具体信息,构建对应的ApiRestResponse统一返回对象;
2.在【GlobalExceptionHandler】中,去处理【MethodArgumentNotValidException异常】:根据该异常的具体信息,去构建ApiRestResponse统一返回对象;(重难点!)
说明:
(1) 方法说明:拦截【MethodArgumentNotValidException异常】,打印日志,去构建ApiRestResponse统一返回对象;
(2) 【getBindingResult()】说明(不准确,可能存在错误);
这个方法是MethodArgumentNotValidException异常类中定义的,作用是获取“校验失败时的错误信息”;该方法的返回值类型是【BindingResult】;(而且,目前有理由相信,getBindingResult()方法是validation参数校验独有的)
(3) 【BindingResult】:这是个接口;
BindingResult类型对象的解释:为了能更好的理解BindingResult,这儿给出了几个回答,目前自己似乎不能得出一个明确的结论,看下这个几个回答,自己体会吧;
● Answer from Dani ;
[!评论] use a BindingResult object as an argument for a validate method of a Validator inside a Controller. Then, you can check this object looking for validation errors:
● Answer from Samuel Liew♦ & Alexander Suraphel;
[!评论] Explain From Spring MVC Form Validation with Annotations Tutorial: [
BindingResult
] is Spring’s object that holds the result of the validation and binding and contains errors that may have occurred. TheBindingResult
must come right after the model object that is validated or else Spring will fail to validate the object and throw an exception. When Spring sees@Valid
, it tries to find the validator for the object being validated. Spring automatically picks up validation annotations if you have “annotation-driven” enabled. Spring then invokes the validator and puts any errors in theBindingResult
and adds the BindingResult to the view model.
● Answer from Ashish Bansal;
[!评论] Well its a sequential process. The Request first treat by FrontController and then moves towards our own customize controller with @Controller annotation. but our controller method is binding bean using modelattribute and we are also performing few validations on bean values. so instead of moving the request to our controller class, FrontController moves it towards one interceptor which creates the temp object of our bean and the validate the values. if validation successful then bind the temp obj values with our actual bean which is stored in @ModelAttribute otherwise if validation fails it does not bind and moves the resp towards error page or wherever u want.
(4) 【BindingResult.hasErrors()】:bindingResult.hasErrors()是为了验证@Validated后面bean里是否有不符合校验条件的错误信息;
(5)【BindingResult.getAllErrors()】(方法返回值类型是【List<ObjectError>】):获取参数校验时的,所有错误;
● 目前,针对Validation参数校验,和BindingResult的理解,还非常浅;以后,有精力、有需要了,再仔细研究吧;
(6)【ObjectError】:可以理解为是一个【封装了Validation校验错误】的对象;(理解,可能不准确……)
(7)【getDefaultMessage()】:获取错误的具体信息;
(8)方法说明:提取错误信息,将其添加到一个List集合中;
(9)方法说明:根据从【MethodArgumentNotValidException异常】中,提取的具体错误信息,构建ApiRestResponse统一返回对象;
3.启动项目,观察效果;
(1)效果;
(2)完善:设置为空时,具体的信息:@NotNull(message=“不能为空”);
为此,我们可以在@NotNull注解中,通过message属性,去指定具体的信息;
四:Summary:
1.当我们在GlobalExceptionHandler中,书写了这个【处理MethodArgumentNotValidException异常的逻辑】后;我们在需要参数校验的地方,就可以放心大胆的去使用Validation参数校验去校验参数了;
2.额外的补充说明: (这儿的总结还是比较重要的)
第一点: 这儿也印证了【SpringBoot电商项目商品分类模块增加目录分类接口】中提到的:【承接数据表的实体类】和【接收参数的实体类】,要分别创建,不要混用;
第二点:
● AddCategoryReq的作用是:【新增时,接收参数】;
● 那么,以后还会遇到【修改时,接收参数】的情况;那么【修改时,参数校验的逻辑】和【新增时,参数校验的逻辑】可能是不同的;
● 为此,我们就可以创建一个UpdateCategoryReq类,然后我们使用UpdateCategoryReq去接收参数;然后,在UpdateCategoryReq定义【修改时,参数校验的逻辑】;
● 这样做,条理就会很清晰,不会互相干扰;