Dubbo 的RPC調用中Consumer 和 Provider端都可以對調用的方法做傳參驗證,參數的驗證可以通過JSR303規范 (Java Specification Requests) 提到的 Bean Validation 方式來驗證,
Dubbo官方也是這么推薦的。
最佳實踐中分包部分提到傳參的數據模型定義在API的jar包中,如果你是這樣做的,那么參數的驗證完全可以在Consumer端完成,這樣一來就可以減少網絡開銷並提早得到失敗結果。
下面的介紹基於 Dubbo2.6.2 + Springboot2.1.3 的環境,內容會在Dubbo官方文檔的基礎上做一些擴展。
Hibernate Validation 是Bean Validation 的一個實現,也是Dubbo官方推薦的實現方式,使用Hibernate Validation需要引入如下依賴:
<dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.1.Final</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.16.Final</version> </dependency> <!-- 若啟動報錯 java.lang.ExceptionInInitializerError 則還需引入如下 el-api 依賴 --> <dependency> <groupId>javax.el</groupId> <artifactId>javax.el-api</artifactId> <version>3.0.0</version> </dependency>
在傳參DTO中加上 "@NotNull" 注解
@Data
public class AddressDto implements Serializable {
@NotNull(message = "地址不能為空")
private String address;
private String city;
private Boolean batch;
}
傳參數據模型中添加需要驗證的注解后, consumer端在RPC調用前會經過一個叫做 ValidationFilter 的過濾器,該過濾期獲取到validator后會調用Dubbo提供的 JValidator 如下validate方法
@Override public void validate(String methodName, Class<?>[] parameterTypes, Object[] arguments) throws Exception { List<Class<?>> groups = new ArrayList<Class<?>>(); String methodClassName = clazz.getName() + "$" + toUpperMethoName(methodName); Class<?> methodClass = null; try { methodClass = Class.forName(methodClassName, false, Thread.currentThread().getContextClassLoader()); groups.add(methodClass); } catch (ClassNotFoundException e) { } Set<ConstraintViolation<?>> violations = new HashSet<ConstraintViolation<?>>(); Method method = clazz.getMethod(methodName, parameterTypes); Class<?>[] methodClasses = null; if (method.isAnnotationPresent(MethodValidated.class)){ methodClasses = method.getAnnotation(MethodValidated.class).value(); groups.addAll(Arrays.asList(methodClasses)); } // add into default group groups.add(0, Default.class); groups.add(1, clazz); // convert list to array Class<?>[] classgroups = groups.toArray(new Class[0]); Object parameterBean = getMethodParameterBean(clazz, method, arguments); if (parameterBean != null) { violations.addAll(validator.validate(parameterBean, classgroups )); } for (Object arg : arguments) { validate(violations, arg, classgroups); } if (!violations.isEmpty()) { logger.error("Failed to validate service: " + clazz.getName() + ", method: " + methodName + ", cause: " + violations); throw new ConstraintViolationException("Failed to validate service: " + clazz.getName() + ", method: " + methodName + ", cause: " + violations, violations); } }
然后在for循環的中調用了另一 validate的私有方法,該方法對傳參DTO的類型做了判斷后 使用了Hibernate Validation 提供的 ValidatorImpl.validate 方法來完成的參數驗證,驗證完之后返回 ConstraintVoilation 的集合,在以上方法最后對集合violations做判斷是否非空,非空則代表驗證失敗, 並在后續拋出 ConstraintViolationException 異常,這個異常在經過你controller層之后,是可以被我們定義的 ControllerAdvice 捕獲到。全局異常處理方式
參考,這樣只需要增加一個 ExceptionHandler 處理ConstraintViolationException 就能將 Validation 中用到的注解的message返回調用端,
@ResponseBody @ExceptionHandler(value = ValidationException.class) public Result handleAddressException(ValidationException e){ log.error(e.getMessage()); String validateMsg = null; // 若判斷是 ConstraintViolationException異常 則取出message信息 if(e instanceof ConstraintViolationException){ ConstraintViolationException cve = (ConstraintViolationException)e; Set<ConstraintViolation<?>> constraintViolations = cve.getConstraintViolations(); for(ConstraintViolation cv : constraintViolations){ validateMsg = cv.getMessage(); break; } return ResultUtil.error(ResultEnum.VALIDATION_FAILURE.getCode(), validateMsg); } return ResultUtil.error(ResultEnum.VALIDATION_FAILURE.getCode(),e.getMessage()); }
最終在調用端獲取到JSON結果如下
{ "requestId": null, "success": false, "business": null, "code": "20001", "message": "地址不能為空", "date": null, "version": null, "obj": null }
參考:
[1] Dubbo官方文檔