Java Dubbo
目前架构是网关直接通过泛化调用Dubbo服务,不同于web Controller使用SpringMVC模块来做到参数注解校验。不过不用担心Dubbo也考虑到了这一点,基于SPI机制提供了ValidationFilter。来看看他是如何实现的。

Dubbo 源码实现

Dubbo SPI定义

什么是Dubbo SPI?简单说是通过文件配置对应class路径后会被执行class里的invoke函数。其中的实现原理大家顺着Dubbo的ExtensionLoader去看下源码就能知道。
分析 Dubbo SPI 源码,扩展 Dubbo Validation (groups) - 图1

ValidationFilter说明

  1. //在哪种服务类型激活
  2. //这里的VALIDATION_KEY=“validation” 也就是我们在SPI中需要把key按这个规定定义
  3. @Activate(group = {CONSUMER, PROVIDER}, value = VALIDATION_KEY, order = 10000)
  4. public class ValidationFilter implements Filter {
  5. private Validation validation;
  6. public void setValidation(Validation validation) {
  7. this.validation = validation;
  8. }
  9. @Override
  10. public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
  11. //如果SPI中定义了validation 那么就进行校验
  12. if (validation != null && !invocation.getMethodName().startsWith("$")
  13. && ConfigUtils.isNotEmpty(invoker.getUrl().getMethodParameter(invocation.getMethodName(), VALIDATION_KEY))) {
  14. try {
  15. //执行参数校验
  16. Validator validator = validation.getValidator(invoker.getUrl());
  17. if (validator != null) {
  18. validator.validate(invocation.getMethodName(), invocation.getParameterTypes(), invocation.getArguments());
  19. }
  20. } catch (RpcException e) {
  21. throw e;
  22. } catch (ValidationException e) {
  23. //抛出异常 这里的ValidationException需要深挖一下,后面会说
  24. // only use exception's message to avoid potential serialization issue
  25. return AsyncRpcResult.newDefaultAsyncResult(new ValidationException(e.getMessage()), invocation);
  26. } catch (Throwable t) {
  27. return AsyncRpcResult.newDefaultAsyncResult(t, invocation);
  28. }
  29. }
  30. return invoker.invoke(invocation);
  31. }
  32. }

基础使用

Maven依赖

SpringBoot项目推荐使用

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-validation</artifactId>
  4. </dependency>

手动依赖

  1. <dependency>
  2. <groupId>javax.el</groupId>
  3. <artifactId>javax.el-api</artifactId>
  4. <version>3.0.0</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.glassfish</groupId>
  8. <artifactId>javax.el</artifactId>
  9. </dependency>
  10. <dependency>
  11. <groupId>javax.validation</groupId>
  12. <artifactId>validation-api</artifactId>
  13. </dependency>
  14. <dependency>
  15. <groupId>org.hibernate</groupId>
  16. <artifactId>hibernate-validator</artifactId>
  17. </dependency>

DTO添加validation定义

  1. import lombok.Data;
  2. import javax.validation.constraints.NotBlank;
  3. import javax.validation.constraints.NotNull;
  4. import java.io.Serializable;
  5. @Data
  6. public class PracticeParam implements Serializable {
  7. @NotNull(message = "periodId不能为空")
  8. private Long periodId;
  9. }

服务者interface

  1. public interface IPracticeService {
  2. boolean practiceAdd(PracticeParam practiceParam);
  3. }

Dubbo RPC单元测试

  1. @SpringBootTest(classes = ClientApplication.class)
  2. @RunWith(SpringRunner.class)
  3. @Slf4j
  4. public class PrecticeTest {
  5. @DubboReference(group = "user")
  6. private IPracticeService practiceLogicService;
  7. @Test
  8. public void add(){
  9. PracticeParam practiceParam=new PracticeParam();
  10. log.info(String.valueOf(practiceLogicService.practiceAdd(practiceParam)));
  11. }
  12. }

测试结果

  1. javax.validation.ValidationException: Failed to validate service: com.xx.contract.IPracticeService, method: practiceAdd, cause: [ConstraintViolationImpl{interpolatedMessage='periodId不能为空', propertyPath=periodId, rootBeanClass=class com.xx.request.PracticeParam, messageTemplate='periodId不能为空'}]

看结果是生效了,不过这和实际项目中还有些距离。不能把这个异常抛给网关,根据项目需要适配上全局异常试试。

源码分析与扩展

Dubbo异常处理

再回到前面SPI文件
分析 Dubbo SPI 源码,扩展 Dubbo Validation (groups) - 图2

ExceotionFilter源码分析

  1. //在服务提供者端生效
  2. @Activate(group = CommonConstants.PROVIDER)
  3. public class ExceptionFilter implements Filter, Filter.Listener {
  4. private Logger logger = LoggerFactory.getLogger(ExceptionFilter.class);
  5. @Override
  6. public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
  7. return invoker.invoke(invocation);
  8. }
  9. @Override
  10. public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {
  11. //异常处理逻辑
  12. if (appResponse.hasException() && GenericService.class != invoker.getInterface()) {
  13. try {
  14. Throwable exception = appResponse.getException();
  15. // directly throw if it's checked exception
  16. if (!(exception instanceof RuntimeException) && (exception instanceof Exception)) {
  17. return;
  18. }
  19. // directly throw if the exception appears in the signature
  20. try {
  21. Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());
  22. Class<?>[] exceptionClassses = method.getExceptionTypes();
  23. for (Class<?> exceptionClass : exceptionClassses) {
  24. if (exception.getClass().equals(exceptionClass)) {
  25. return;
  26. }
  27. }
  28. } catch (NoSuchMethodException e) {
  29. return;
  30. }
  31. // for the exception not found in method's signature, print ERROR message in server's log.
  32. logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + exception.getClass().getName() + ": " + exception.getMessage(), exception);
  33. // directly throw if exception class and interface class are in the same jar file.
  34. String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
  35. String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
  36. if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)) {
  37. return;
  38. }
  39. // directly throw if it's JDK exception
  40. String className = exception.getClass().getName();
  41. if (className.startsWith("java.") || className.startsWith("javax.")) {
  42. return;
  43. }
  44. // directly throw if it's dubbo exception
  45. if (exception instanceof RpcException) {
  46. return;
  47. }
  48. // otherwise, wrap with RuntimeException and throw back to the client
  49. //重点时这句,替换异常信息
  50. appResponse.setException(new RuntimeException(StringUtils.toString(exception)));
  51. } catch (Throwable e) {
  52. logger.warn("Fail to ExceptionFilter when called by " + RpcContext.getContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);
  53. }
  54. }
  55. }
  56. @Override
  57. public void onError(Throwable e, Invoker<?> invoker, Invocation invocation) {
  58. logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);
  59. }
  60. // For test purpose
  61. public void setLogger(Logger logger) {
  62. this.logger = logger;
  63. }
  64. }

基础使用

定义SPI

项目结构说明:
分析 Dubbo SPI 源码,扩展 Dubbo Validation (groups) - 图3
文件定义(Dubbo SPI需要严格按如下路径和文件名)

  1. src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter

文件内容

  1. validation=com.xx.xx.config.DubboValidationFilter
  2. exception=com.xx.xx.config.DubboExceptionFilter

自定义DubboValidationFilter

  1. import com.xx.exception.ParamException;
  2. import org.apache.dubbo.common.extension.Activate;
  3. import org.apache.dubbo.common.utils.ConfigUtils;
  4. import org.apache.dubbo.rpc.*;
  5. import org.apache.dubbo.validation.Validation;
  6. import org.apache.dubbo.validation.Validator;
  7. import javax.validation.ConstraintViolation;
  8. import javax.validation.ConstraintViolationException;
  9. import java.util.Set;
  10. import static org.apache.dubbo.common.constants.CommonConstants.PROVIDER;
  11. import static org.apache.dubbo.common.constants.FilterConstants.VALIDATION_KEY;
  12. @Activate(group = {PROVIDER}, value = VALIDATION_KEY, order = -1)
  13. public class DubboValidationFilter implements Filter {
  14. private Validation validation;
  15. public void setValidation(Validation validation) {
  16. this.validation = validation;
  17. }
  18. @Override
  19. public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
  20. if (validation != null && !invocation.getMethodName().startsWith("$")
  21. && ConfigUtils.isNotEmpty(invoker.getUrl().getMethodParameter(invocation.getMethodName(), VALIDATION_KEY))) {
  22. try {
  23. Validator validator = validation.getValidator(invoker.getUrl());
  24. if (validator != null) {
  25. //挖掘点 validate函数的源码
  26. validator.validate(invocation.getMethodName(), invocation.getParameterTypes(), invocation.getArguments());
  27. }
  28. }
  29. //Dubbo源码里捕获的是ValidationException这个异常,原始信息变成了字符串,所以接下来
  30. //通过JValidator源码分析进行如下扩展
  31. catch (ConstraintViolationException e) {
  32. //获取我们的异常,这里的异常时集合的,因为我们参数可能多个都不通过
  33. StringBuilder message = new StringBuilder();
  34. Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
  35. for (ConstraintViolation<?> violation : violations) {
  36. //这里只获取第一个不通过的原因
  37. message.append(violation.getMessage().concat(";"));
  38. break;
  39. }
  40. //项目自定义异常类型,网关可以捕获到该异常
  41. throw new ParamException(message.toString());
  42. } catch (RpcException e) {
  43. throw e;
  44. } catch (Throwable t) {
  45. return AsyncRpcResult.newDefaultAsyncResult(t, invocation);
  46. }
  47. }
  48. return invoker.invoke(invocation);
  49. }
  50. }

JValidator源码分析

  1. //Validated 校验过程
  2. public void validate(String methodName, Class<?>[] parameterTypes, Object[] arguments) throws Exception {
  3. List<Class<?>> groups = new ArrayList<>();
  4. Class<?> methodClass = methodClass(methodName);
  5. if (methodClass != null) {
  6. groups.add(methodClass);
  7. }
  8. //异常返回信息 violations
  9. Set<ConstraintViolation<?>> violations = new HashSet<>();
  10. Method method = clazz.getMethod(methodName, parameterTypes);
  11. Class<?>[] methodClasses;
  12. if (method.isAnnotationPresent(MethodValidated.class)){
  13. methodClasses = method.getAnnotation(MethodValidated.class).value();
  14. groups.addAll(Arrays.asList(methodClasses));
  15. }
  16. // add into default group
  17. groups.add(0, Default.class);
  18. groups.add(1, clazz);
  19. // convert list to array
  20. Class<?>[] classgroups = groups.toArray(new Class[groups.size()]);
  21. Object parameterBean = getMethodParameterBean(clazz, method, arguments);
  22. if (parameterBean != null) {
  23. violations.addAll(validator.validate(parameterBean, classgroups ));
  24. }
  25. for (Object arg : arguments) {
  26. validate(violations, arg, classgroups);
  27. }
  28. if (!violations.isEmpty()) {
  29. logger.error("Failed to validate service: " + clazz.getName() + ", method: " + methodName + ", cause: " + violations);
  30. //这里原始异常是它,所以我们需要捕获它,得到violations
  31. throw new ConstraintViolationException("Failed to validate service: " + clazz.getName() + ", method: " + methodName + ", cause: " + violations, violations);
  32. }
  33. }

自定义DubboExceptionFilter

  1. @Slf4j
  2. @Activate(group = CommonConstants.PROVIDER)
  3. public class DubboExceptionFilter extends ExceptionFilter {
  4. @Override
  5. public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
  6. return invoker.invoke(invocation);
  7. }
  8. @Override
  9. public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {
  10. log.error("dubbo global exception ---------->{}", appResponse.getException());
  11. if (appResponse.hasException() && GenericService.class != invoker.getInterface()) {
  12. try {
  13. Throwable exception = appResponse.getException();
  14. // 自定义异常处理
  15. if (exception instanceof ParamException) {
  16. //按项目log收集规范输出
  17. log.error("dubbo service exception ---------->{}", exception);
  18. return;
  19. }
  20. ......
  21. } catch (Throwable e) {
  22. log.warn("Fail to ExceptionFilter when called by " + RpcContext.getContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);
  23. }
  24. }
  25. }
  26. }

PRC单元测试

  1. org.apache.dubbo.remoting.RemotingException: com.xx.exception.ParamException: periodId不能为空;
  2. com.xx.exception.ParamException: periodId不能为空;

高级进阶

在业务里一个DTO对象会用于新增或更新,在新增时不需要主键ID,在更新时需要主键ID。
就需要引入分组的概念了。

定义validation groups,在API模块中定义两个分组。

  1. //用于新增
  2. public interface InsertValidation {
  3. }
  4. //用于更新
  5. public interface UpdateValidation {
  6. }

定义 DTO

  1. @Data
  2. public class PracticeParam implements Serializable {
  3. //只用于更新
  4. @NotNull(groups={UpdateValidation.class},message = "id不能为空")
  5. private Integer id;
  6. // 如果两组校验都需要可以省去group的定义,完整的如下
  7. // @NotBlank(groups={InsertValidation.class, UpdateValidation.class},message = "名称不能为空")
  8. @NotNull(message = "periodId不能为空")
  9. private Long periodId;
  10. }

服务提供者interface

  1. public interface IPracticeService {
  2. //因为periodId参数是默认不区分组的,所以这里省去了Validated注解
  3. boolean practiceAdd(PracticeParam practiceParam);
  4. boolean practiceEdit(@Validated(value = {UpdateValidation.class}) PracticeParam practiceParam);
  5. }

看看真实运行情况。

生产环境验证

分析 Dubbo SPI 源码,扩展 Dubbo Validation (groups) - 图4