SpringBoot validator參數(shù)驗(yàn)證restful自定義錯(cuò)誤碼響應(yīng)方式
validator參數(shù)驗(yàn)證restful自定義錯(cuò)誤碼響應(yīng)
關(guān)于spring web應(yīng)用中關(guān)于如何使用 Bean Validation API和hibernate-validator的文章已經(jīng)很多,本文就不再重復(fù)敘述,今天要介紹的重點(diǎn)是在SpringBoot restful服務(wù)中如何根據(jù)不同驗(yàn)證錯(cuò)誤響應(yīng)不同的自定義錯(cuò)誤碼。下面直接上代碼。
一、定義restful統(tǒng)一結(jié)果返回
阿里java開(kāi)發(fā)手冊(cè)中定義的一段參考【“對(duì)于公司外的 http/api 開(kāi)放接口必須使用“錯(cuò)誤碼”; 而應(yīng)用內(nèi)部推薦異常拋出;跨應(yīng)用間 RPC 調(diào)用優(yōu)先考慮使用 Result 方式,封裝 isSuccess()方法、 “錯(cuò)誤碼”、“錯(cuò)誤簡(jiǎn)短信息”?!?。因此這里也定義個(gè)返回結(jié)構(gòu)。
public class CommonResult<T> implements Serializable { /** * serialVersionUID:. */ private static final long serialVersionUID = -7268040542410707954L; /** * 是否成功 */ private boolean success = false; /** * 返回信息 */ private String message; /** * 裝在數(shù)據(jù) */ private T data; /** * 錯(cuò)誤代碼 */ private String code; /** * 默認(rèn)構(gòu)造器 */ public CommonResult(){ } /** * * @param success * 是否成功 * @param message * 返回的消息 */ public CommonResult(boolean success, String message){ this.success = success; this.message = message; } /** * * @param success * 是否成功 */ public CommonResult(boolean success){ this.success = success; } /** * * @param code error code * @param message success or error messages */ public CommonResult(String code,String message){ this.code = code; this.message = message; } /** * * @param success * 是否成功 * @param message * 消息 * @param data * 數(shù)據(jù) */ public CommonResult(boolean success, String message, T data){ this.success = success; this.message = message; this.data = data; } //省略get set }
二、定義一個(gè)錯(cuò)誤碼枚舉
在有需要國(guó)際化的項(xiàng)目,當(dāng)然選擇通過(guò)i18n來(lái)配置更好,此處為了簡(jiǎn)單直接采用枚舉定義。這里定義的錯(cuò)誤僅供參考,不同公司每個(gè)應(yīng)用在實(shí)際情況下可能都不大一樣。
/** * 錯(cuò)誤代碼枚舉類(lèi) * */ public enum ErrorCodeEnum { SUCCESS("0000", "success"), PARAM_EMPTY("1001", "必選參數(shù)為空"), PARAM_ERROR("1002", "參數(shù)格式錯(cuò)誤"), UNKNOWN_ERROR("9999", "系統(tǒng)繁忙,請(qǐng)稍后再試...."); private String code; private String desc; ErrorCodeEnum(String code, String desc) { this.code = code; this.desc = desc; } public String getCode() { return this.code; } public String getDesc() { return desc; } @Override public String toString() { return "[" + this.code + "]" + this.desc; } }
三、靜態(tài)封裝CommonResult
靜態(tài)封裝CommonResult主要是方便在項(xiàng)目中快速根據(jù)邏輯寫(xiě)返回結(jié)果代碼。
/** * 公共響應(yīng)結(jié)果成功失敗的靜態(tài)方法調(diào)用 * */ public class ResultUtil { /** * return success * * @param data * @return */ public static <T> CommonResult<T> returnSuccess(T data) { CommonResult<T> result = new CommonResult(); result.setCode(ErrorCodeEnum.SUCCESS.getCode()); result.setSuccess(true); result.setData(data); result.setMessage(ErrorCodeEnum.SUCCESS.getDesc()); return result; } /** * return error * * @param code error code * @param msg error message * @return */ public static CommonResult returnError(String code, String msg) { CommonResult result = new CommonResult(); result.setCode(code); result.setData(""); result.setMessage(msg); return result; } /** * use enum * * @param status * @return */ public static CommonResult returnError(ErrorCodeEnum status) { return returnError(status.getCode(), status.getDesc()); } }
四、定義BaseController來(lái)處理驗(yàn)證錯(cuò)誤自定義錯(cuò)誤碼返回
/** * BaseController * */ public abstract class BaseController { private static final Logger LOGGER = LoggerFactory.getLogger(BaseController.class); /** * validate params * * @param bindingResult * @return */ protected CommonResult validParams(BindingResult bindingResult) { if (bindingResult.hasErrors()) { FieldError fieldError = bindingResult.getFieldError(); return processBindingError(fieldError); } return ResultUtil.returnSuccess(""); } /** * 根據(jù)spring binding 錯(cuò)誤信息自定義返回錯(cuò)誤碼和錯(cuò)誤信息 * * @param fieldError * @return */ private CommonResult processBindingError(FieldError fieldError) { String code = fieldError.getCode(); LOGGER.debug("validator error code: {}", code); switch (code) { case "NotEmpty": return ResultUtil.returnError(ErrorCodeEnum.PARAM_EMPTY.getCode(), fieldError.getDefaultMessage()); case "NotBlank": return ResultUtil.returnError(ErrorCodeEnum.PARAM_EMPTY.getCode(), fieldError.getDefaultMessage()); case "NotNull": return ResultUtil.returnError(ErrorCodeEnum.PARAM_EMPTY.getCode(), fieldError.getDefaultMessage()); case "Pattern": return ResultUtil.returnError(ErrorCodeEnum.PARAM_ERROR.getCode(), fieldError.getDefaultMessage()); case "Min": return ResultUtil.returnError(ErrorCodeEnum.PARAM_ERROR.getCode(), fieldError.getDefaultMessage()); case "Max": return ResultUtil.returnError(ErrorCodeEnum.PARAM_ERROR.getCode(), fieldError.getDefaultMessage()); case "Length": return ResultUtil.returnError(ErrorCodeEnum.PARAM_ERROR.getCode(), fieldError.getDefaultMessage()); case "Range": return ResultUtil.returnError(ErrorCodeEnum.PARAM_ERROR.getCode(), fieldError.getDefaultMessage()); case "Email": return ResultUtil.returnError(ErrorCodeEnum.PARAM_ERROR.getCode(), fieldError.getDefaultMessage()); case "DecimalMin": return ResultUtil.returnError(ErrorCodeEnum.PARAM_ERROR.getCode(), fieldError.getDefaultMessage()); case "DecimalMax": return ResultUtil.returnError(ErrorCodeEnum.PARAM_ERROR.getCode(), fieldError.getDefaultMessage()); case "Size": return ResultUtil.returnError(ErrorCodeEnum.PARAM_ERROR.getCode(), fieldError.getDefaultMessage()); case "Digits": return ResultUtil.returnError(ErrorCodeEnum.PARAM_ERROR.getCode(), fieldError.getDefaultMessage()); case "Past": return ResultUtil.returnError(ErrorCodeEnum.PARAM_ERROR.getCode(), fieldError.getDefaultMessage()); case "Future": return ResultUtil.returnError(ErrorCodeEnum.PARAM_ERROR.getCode(), fieldError.getDefaultMessage()); default: return ResultUtil.returnError(ErrorCodeEnum.UNKNOWN_ERROR); } } }
五、驗(yàn)證實(shí)例
這里直接給出一個(gè)簡(jiǎn)單的參數(shù)驗(yàn)證例子。
Controller繼承上面寫(xiě)的BaseController
/** * 關(guān)于Validator使用測(cè)試 * */ @RestController @RequestMapping("validator") public class ValidatorTestController extends BaseController { private static final Logger LOGGER = LoggerFactory.getLogger(ValidatorTestController.class); /** * validate驗(yàn)證測(cè)試 * * @param leader * @param bindingResult * @return */ @PostMapping("/test") public CommonResult testSimpleValidate(@Valid @RequestBody Leader leader, BindingResult bindingResult) { LOGGER.debug("ReqParams:{}", JSON.toJSONString(leader)); CommonResult result = validParams(bindingResult); if (!result.isSuccess()) { return result; } return ResultUtil.returnSuccess(""); } }
入?yún)?duì)象Leader代碼
public class Leader { /** * 姓名 */ @NotEmpty private String name; /** * 生日 */ @Pattern(regexp = "^[0-9]{4}-[0-9]{2}-[0-9]{2}$", message = "出生日期格式不正確") private String birthday; /** * 年齡 */ @Min(value = 0) private Integer age; //省略gettes and setters }
這時(shí)項(xiàng)目已經(jīng)已經(jīng)完全可以根據(jù)驗(yàn)證錯(cuò)誤來(lái)返回自定義的錯(cuò)誤碼和提示了。
本例所涉及源代碼:https://github.com/shalousun/api-doc-test
小結(jié)一下
在一些對(duì)外服務(wù)提供restful的應(yīng)用中,根據(jù)不同的驗(yàn)證錯(cuò)誤返回其實(shí)是避免不了。當(dāng)然實(shí)現(xiàn)的方式可以有種,而本文所采用的方式相對(duì)來(lái)說(shuō)簡(jiǎn)單易懂。
使用validator-api驗(yàn)證springboot的參數(shù)
作為服務(wù)端開(kāi)發(fā),驗(yàn)證前端傳入的參數(shù)的合法性是一個(gè)必不可少的步驟,但是驗(yàn)證參數(shù)是一個(gè)基本上是一個(gè)體力活,而且冗余代碼繁多,也影響代碼的可閱讀性,所以有沒(méi)有一個(gè)比較優(yōu)雅的方式來(lái)解決這個(gè)問(wèn)題?
這么簡(jiǎn)單的問(wèn)題當(dāng)然早就有大神遇到并且解決了,這一篇文章主要講一下解決基于spring-boot的驗(yàn)證參數(shù)的比較好的方法:利用validator-api來(lái)進(jìn)行驗(yàn)證參數(shù)。
在spring-boot-starter-web包里面有hibernate-validator包,它提供了一系列驗(yàn)證各種參數(shù)的方法,所以說(shuō)spring-boot已經(jīng)幫我們想好要怎么解決這個(gè)問(wèn)題了。
這篇文章針對(duì)spring-boot里面的spring-mvc介紹三種方式來(lái)驗(yàn)證參數(shù)。
一、這個(gè)方法在網(wǎng)上大部分都可以查到
先假設(shè)我們的restful的接口接受一個(gè)GradeAndClassroomModel類(lèi)型的對(duì)象,并且這個(gè)類(lèi)被定義成
@Data public class GradeAndClassroomModel { @Range(min = 1, max = 9, message = "年級(jí)只能從1-9") private int grade; @Range(min = 1, max = 99, message = "班級(jí)只能從1-99") private int classroomNumber; }
利用validator提供的一系列注解,比如本例中的@Range,就可以表示參數(shù)的范圍和出錯(cuò)時(shí)候的提示信息。還有很多其他注解,這里就不一一列出
然后我們的Controller層的代碼為
@RequestMapping(value = "/paramErrorTest", method = RequestMethod.GET) public String paramErrorTest( @Valid @ModelAttribute GradeAndClassroomModel gradeAndClassroomModel, BindingResult result) { return classroomService.getTeacherName(gradeAndClassroomModel.getGrade(), gradeAndClassroomModel.getClassroomNumber()); }
其中如果驗(yàn)證出錯(cuò),result對(duì)象里面就會(huì)有錯(cuò)誤信息,然后可以自己進(jìn)行處理。
二、針對(duì)上面的例子
會(huì)有人說(shuō),就兩個(gè)參數(shù),為什么要作為對(duì)象呢?會(huì)不會(huì)太麻煩?確實(shí),如果只有少數(shù)對(duì)象,直接把參數(shù)寫(xiě)到Controller層,然后在Controller層進(jìn)行驗(yàn)證就可以了。
@RequestMapping(value = "/teacherName", method = RequestMethod.GET) public String teacherName( @Range(min = 1, max = 9, message = "年級(jí)只能從1-9") @RequestParam(name = "grade", required = true) int grade, @Min(value = 1, message = "班級(jí)最小只能1") @Max(value = 99, message = "班級(jí)最大只能99") @RequestParam(name = "classroom", required = true) int classroom) { return classroomService.getTeacherName(grade, classroom); }
如果直接把validator提供的注解移除來(lái)寫(xiě)到請(qǐng)求參數(shù)上面的話(huà)是不是就可以了呢?答案是錯(cuò),為什么這樣不能成功的驗(yàn)證參數(shù)呢?具體原因大家可以參考官方文檔:
上面的文檔已經(jīng)說(shuō)的很清楚了,所以我們需要?jiǎng)?chuàng)建一個(gè)Bean
@Bean public MethodValidationPostProcessor methodValidationPostProcessor() { return new MethodValidationPostProcessor(); }
然后在類(lèi)方法上面加上注解@Validated
@RestController @RequestMapping("/spring-boot/classroom") @Validated public class ClassroomController { ... }
然后之前沒(méi)有生效的注解@Range、@Min、@Max等validator包里面提供的注解就可以生效了。
三、估計(jì)到了這里又會(huì)有人問(wèn)
如果validator包里面注解不能滿(mǎn)足我們的需求,我們是否可以自己定義參數(shù)驗(yàn)證的邏輯。答案是肯定的,我們可以利用
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.FIELD}) @Constraint(validatedBy = {Validator.class}) public @interface ParamValidator { String message() default "Parameter error!"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
和
public class Validator implements ConstraintValidator<ParamValidator, Object> { ... }
組合進(jìn)行自定義,具體的例子網(wǎng)上其他文章就很多了,這里就不進(jìn)行詳細(xì)的例子了,但是最終使用的時(shí)候就是
@RequestMapping(value = "/paramValidator", method = RequestMethod.GET) public String paramValidator( @ParamValidator(isRequired = true, desc = "年級(jí)", range = "int:1~9", message = "年級(jí)只能從1-9") @RequestParam(name = "grade", required = true) int grade, @ParamValidator(isRequired = true, desc = "班級(jí)", range = "int:1~99", message = "班級(jí)只能從1-99") @RequestParam(name = "classroom", required = true) int classroom) { return classroomService.getTeacherName(grade, classroom); }
另外不要忘記方法二里面里面提到的MethodValidationPostProcessor這個(gè)bean,如果沒(méi)有初始化這個(gè)bean,自定義的驗(yàn)證方法也不會(huì)執(zhí)行。驗(yàn)證邏輯會(huì)失效。
是不是通過(guò)這樣寫(xiě)注解的方式來(lái)驗(yàn)證進(jìn)行請(qǐng)求的參數(shù),代碼邏輯更佳清晰和優(yōu)雅?表達(dá)的含義也會(huì)更佳清楚?并且沒(méi)有了大量重復(fù)的類(lèi)似的驗(yàn)證代碼。
Ps:這里的代碼都是基于spring-mvc框架來(lái)試驗(yàn)的,如果有人并沒(méi)有使用spring-mvc作為rest框架,而是使用jersey來(lái)作為rest框架的話(huà),可能一些細(xì)節(jié)方面需要調(diào)整, 但是這三種方案應(yīng)該都是可以兼容的。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
spring boot+mybatis 多數(shù)據(jù)源切換(實(shí)例講解)
下面小編就為大家?guī)?lái)一篇spring boot+mybatis 多數(shù)據(jù)源切換(實(shí)例講解)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-09-09java調(diào)用chatgpt接口來(lái)實(shí)現(xiàn)專(zhuān)屬于自己的人工智能助手
這篇文章主要介紹了用java來(lái)調(diào)用chatget的接口,實(shí)現(xiàn)自己的聊天機(jī)器人,對(duì)人工智能感興趣的小伙伴可以參考閱讀2023-03-03Java中BigDecimal,DateFormatter?和迭代器的"陷阱"
這篇文章主要介紹了Java中BigDecimal,DateFormatter?和迭代器的"陷阱",文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,感興趣的小伙伴可以參考一下2022-06-06Java?synchronized輕量級(jí)鎖實(shí)現(xiàn)過(guò)程淺析
這篇文章主要介紹了Java synchronized輕量級(jí)鎖實(shí)現(xiàn)過(guò)程,synchronized是Java里的一個(gè)關(guān)鍵字,起到的一個(gè)效果是"監(jiān)視器鎖",它的功能就是保證操作的原子性,同時(shí)禁止指令重排序和保證內(nèi)存的可見(jiàn)性2023-02-02Java圖形化編程中的鍵盤(pán)事件設(shè)計(jì)簡(jiǎn)介
這篇文章主要介紹了Java圖形化編程中的鍵盤(pán)事件設(shè)計(jì),是Java的GUI編程當(dāng)中的基礎(chǔ)部分,需要的朋友可以參考下2015-10-10