前言
在web應用中,請求處理時,出現異常是非常常見的。所以當應用出現各類異常時,進行異常的捕獲或者二次處理(比如sql異常正常是不能外拋)是非常必要的,比如在開發對外api服務時,約定了響應的參數格式,如
respCode
、respMsg
,調用方根據錯誤碼進行自己的業務邏輯。本章節就重點講解下統一異常和數據校驗處理。
springboot
中,默認在發送異常時,會跳轉值/error
請求進行錯誤的展現,根據不同的Content-Type
展現不同的錯誤結果,如json請求時,直接返回json格式參數。
瀏覽器訪問異常時:
使用postman
訪問時:
統一異常處理
顯然,默認的異常頁是對用戶或者調用者而言都是不友好的,所以一般上我們都會進行實現自己業務的異常提示信息。
創建全局的統一異常處理類
利用
@ControllerAdvice
和@ExceptionHandler
定義一個統一異常處理類
-
@ControllerAdvice:控制器增強,使@ExceptionHandler、@InitBinder、@ModelAttribute注解的方法應用到所有的 @RequestMapping注解的方法。
-
@ExceptionHandler:異常處理器,此注解的作用是當出現其定義的異常時進行處理的方法
創建異常類:CommonExceptionHandler
@ControllerAdvice
public class CommonExceptionHandler {
/**
* 攔截Exception類的異常
* @param e
* @return
*/
@ExceptionHandler(Exception.class)
@ResponseBody
public Map<String,Object> exceptionHandler(Exception e){
Map<String,Object> result = new HashMap<String,Object>();
result.put("respCode", "9999");
result.put("respMsg", e.getMessage());
//正常開發中,可創建一個統一響應實體,如CommonResp
return result;
}
}
多余不同異常(如自定義異常),需要進行不同的異常處理時,可編寫多個exceptionHandler方法,注解ExceptionHandler
指定處理的異常類,如
/**
* 攔截 CommonException 的異常
* @param ex
* @return
*/
@ExceptionHandler(CommonException.class)
@ResponseBody
public Map<String,Object> exceptionHandler(CommonException ex){
log.info("CommonException:{}({})",ex.getMsg(), ex.getCode());
Map<String,Object> result = new HashMap<String,Object>();
result.put("respCode", ex.getCode());
result.put("respMsg", ex.getMsg());
return result;
}
由於加入了@ResponseBody
,所以返回的是json
格式,
說明異常已經被攔截了。
可攔截不同的異常,進行不同的異常提示,比如NoHandlerFoundException
、HttpMediaTypeNotSupportedException
、AsyncRequestTimeoutException
等等,這里就不列舉了,讀者可自己加入后實際操作下。
對於返回頁面時,返回ModelAndView
即可,如
@ExceptionHandler(value = Exception.class)
public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
ModelAndView mav = new ModelAndView();
mav.addObject("exception", e);
mav.addObject("url", req.getRequestURL());
mav.setViewName(DEFAULT_ERROR_VIEW);
return mav;
}
由於工作中都是才有前后端分離開發模式,所以一般上都沒有直接返回資源頁的需求了,一般上都是返回固定的響應格式,如respCode
、respMsg
、data
,前端通過判斷respCode
的值進行業務判斷,是彈窗還是跳轉頁面。
數據校驗
在web開發時,對於請求參數,一般上都需要進行參數合法性校驗的,原先的寫法時一個個字段一個個去判斷,這種方式太不通用了,所以java的
JSR 303: Bean Validation
規范就是解決這個問題的。
JSR 303
只是個規范,並沒有具體的實現,目前通常都是才有hibernate-validator
進行統一參數校驗。
JSR303定義的校驗類型
Constraint | 詳細信息 |
---|---|
@Null |
被注釋的元素必須為 null |
@NotNull |
被注釋的元素必須不為 null |
@AssertTrue |
被注釋的元素必須為 true |
@AssertFalse |
被注釋的元素必須為 false |
@Min(value) |
被注釋的元素必須是一個數字,其值必須大於等於指定的最小值 |
@Max(value) |
被注釋的元素必須是一個數字,其值必須小於等於指定的最大值 |
@DecimalMin(value) |
被注釋的元素必須是一個數字,其值必須大於等於指定的最小值 |
@DecimalMax(value) |
被注釋的元素必須是一個數字,其值必須小於等於指定的最大值 |
@Size(max, min) |
被注釋的元素的大小必須在指定的范圍內 |
@Digits (integer, fraction) |
被注釋的元素必須是一個數字,其值必須在可接受的范圍內 |
@Past |
被注釋的元素必須是一個過去的日期 |
@Future |
被注釋的元素必須是一個將來的日期 |
@Pattern(value) |
被注釋的元素必須符合指定的正則表達式 |
Hibernate Validator 附加的 constraint
Constraint | 詳細信息 |
---|---|
@Email |
被注釋的元素必須是電子郵箱地址 |
@Length |
被注釋的字符串的大小必須在指定的范圍內 |
@NotEmpty |
被注釋的字符串的必須非空 |
@Range |
被注釋的元素必須在合適的范圍內 |
創建實體類
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DemoReq {
@NotBlank(message="code不能為空")
String code;
@Length(max=10,message="name長度不能超過10")
String name;
}
然后在控制層方法里,加入@Valid
即可,這樣在訪問前,會對請求參數進行檢驗。
@GetMapping("/demo/valid")
public String demoValid(@Valid DemoReq req) {
return req.getCode() + "," + req.getName();
}
啟動,后訪問http://127.0.0.1:8080/demo/valid
加上正確參數后,http://127.0.0.1:8080/demo/valid?code=3&name=s
這樣數據的統一校驗就完成了,對於其他注解的使用,大家可自行谷歌下,基本上都很簡單的,對於已有的注解無法滿足校驗需要時,也可進行自定義注解的開發,一下簡單講解下,自定義注解的編寫
不使用@valid的情況下,也可利用編程的方式編寫一個工具類,進行實體參數校驗
public class ValidatorUtil {
private static Validator validator = ((HibernateValidatorConfiguration) Validation
.byProvider(HibernateValidator.class).configure()).failFast(true).buildValidatorFactory().getValidator();
/**
* 實體校驗
*
* @param obj
* @throws CommonException
*/
public static <T> void validate(T obj) throws CommonException {
Set<ConstraintViolation<T>> constraintViolations = validator.validate(obj, new Class[0]);
if (constraintViolations.size() > 0) {
ConstraintViolation<T> validateInfo = (ConstraintViolation<T>) constraintViolations.iterator().next();
// validateInfo.getMessage() 校驗不通過時的信息,即message對應的值
throw new CommonException("0001", validateInfo.getMessage());
}
}
}
使用
@GetMapping("/demo/valid")
public String demoValid(@Valid DemoReq req) {
//手動校驗
ValidatorUtil.validate(req);
return req.getCode() + "," + req.getName();
}
自定義校驗注解
自定義注解,主要時實現
ConstraintValidator
的處理類即可,這里已編寫一個校驗常量的注解為例:參數值只能為特定的值。
自定義注解
@Documented
//指定注解的處理類
@Constraint(validatedBy = {ConstantValidatorHandler.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
public @interface Constant {
String message() default "{constraint.default.const.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String value();
}
注解處理類
public class ConstantValidatorHandler implements ConstraintValidator<Constant, String> {
private String constant;
@Override
public void initialize(Constant constraintAnnotation) {
//獲取設置的字段值
this.constant = constraintAnnotation.value();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
//判斷參數是否等於設置的字段值,返回結果
return constant.equals(value);
}
}
使用
@Constant(message = "verson只能為1.0",value="1.0")
String version;
運行:
此時,自定義注解已生效。大家可根據實際需求進行開發。
大家看到在校驗不通過時,返回的異常信息是不友好的,此時可利用統一異常處理,對校驗異常進行特殊處理,特別說明下,對於異常處理類
共有以下幾種情況(被@RequestBody和@RequestParam注解的請求實體,校驗異常類是不同的)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Map<String,Object> handleBindException(MethodArgumentNotValidException ex) {
FieldError fieldError = ex.getBindingResult().getFieldError();
log.info("參數校驗異常:{}({})", fieldError.getDefaultMessage(),fieldError.getField());
Map<String,Object> result = new HashMap<String,Object>();
result.put("respCode", "01002");
result.put("respMsg", fieldError.getDefaultMessage());
return result;
}
@ExceptionHandler(BindException.class)
public Map<String,Object> handleBindException(BindException ex) {
//校驗 除了 requestbody 注解方式的參數校驗 對應的 bindingresult 為 BeanPropertyBindingResult
FieldError fieldError = ex.getBindingResult().getFieldError();
log.info("必填校驗異常:{}({})", fieldError.getDefaultMessage(),fieldError.getField());
Map<String,Object> result = new HashMap<String,Object>();
result.put("respCode", "01002");
result.put("respMsg", fieldError.getDefaultMessage());
return result;
}
啟動后,提示就友好了
所以統一異常還是很有必要的。
總結
本章節主要是闡述了統一異常處理和數據的合法性校驗,同時簡單實現了一個自定義的注解類,大家在碰見已有注解無法解決時,可通過自定義的形式進行,當然對於通用而已,利用
@Pattern(正則表達式)
基本上都是可以實現的。
最后
目前互聯網上很多大佬都有
springboot
系列教程,如有雷同,請多多包涵了。本文是作者在電腦前一字一句敲的,每一步都是實踐的。若文中有所錯誤之處,還望提出,謝謝。
老生常談
- 個人QQ:
499452441
- 微信公眾號:
lqdevOps
完整示例:chapter-8
原文地址:http://blog.lqdev.cn/2018/07/20/springboot/chapter-eight/