Dubbo接口参数校验的正确姿势


1.前言

由于之前的文章分享了如下这篇文章:
@Validated或@Valid参数注解校验、自定义手机号注解检验及优雅统一异常处理
https://mp.weixin.qq.com/s/o3SqNXO8BFEq2YsrlcEixA
https://blog.csdn.net/qq_34905631/article/details/137821646?spm=1001.2014.3001.5501
后面在项目中使用dubbo接口,就想能不能让dubbo接口也能参数校验,那么就不用在代码中写很多的if/else的参数校验了,直接dubbo的参数校验就给做了,那这种接口代码是不是又优雅、干净整洁了很多,于是乎我就上网开始看了一个些千篇一律的文章,没有一篇可以的,然后就经过不断的尝试之后,dubbo接口参数校验的正确姿势还是被我搞出来了,下面就分享给大家。


2.代码示例

2.1版本

dubbo版本2.7.13,至于项目中如何整合dubbo之前的文章中都有分享或者可以去dubbo官网查看。
在dubbo2.7.x版本duboo开始进入Apache,所以之前下面自定义的注解filter的名字是如下的名字:
org.apache.dubbo.rpc.Filter
不在是之前alibaba那种命名了,这里需要特别注意。


2.2校验依赖

在dubbo的api接口的项目pom中引入校验依赖跟上面之前分享的文章的依赖可以说是一样
        方式一;        <dependency>            <groupId>org.springframework.bootgroupId>            <artifactId>spring-boot-starter-validationartifactId>            <version>2.3.12.RELEASEversion>        dependency>        方式二;        <dependency>            <groupId>javax.validationgroupId>            <artifactId>validation-apiartifactId>            <version>2.0.1.Finalversion>        dependency>        <dependency>            <groupId>org.hibernategroupId>            <artifactId>hibernate-validatorartifactId>            <version>6.2.0.Finalversion>        dependency>
方式一和方式二任选一种引入依赖即可。


2.3自定义DubboValidationFilter

在resources下新建一个目录:META-INF.dubbo下面新建一个如下文件;
org.apache.dubbo.rpc.Filter

文件内容如下;
dubboValidationFilter=xx.xx.xxxx.config.DubboValidationFilter
DubboValidationFilter类如下:
package xxxx.config;
import cn.hutool.core.collection.CollectionUtil;import xxxx.RestResponse;import lombok.extern.slf4j.Slf4j;import org.apache.dubbo.common.extension.Activate;import org.apache.dubbo.common.utils.ConfigUtils;import org.apache.dubbo.rpc.AsyncRpcResult;import org.apache.dubbo.rpc.Filter;import org.apache.dubbo.rpc.Invocation;import org.apache.dubbo.rpc.Invoker;import org.apache.dubbo.rpc.Result;import org.apache.dubbo.rpc.RpcException;import org.apache.dubbo.validation.Validation;import org.apache.dubbo.validation.Validator;
import javax.validation.ConstraintViolation;import javax.validation.ConstraintViolationException;import javax.validation.ValidationException;import java.util.ArrayList;import java.util.List;import java.util.Set;
import static org.apache.dubbo.common.constants.CommonConstants.CONSUMER;import static org.apache.dubbo.common.constants.CommonConstants.PROVIDER;import static org.apache.dubbo.common.constants.FilterConstants.VALIDATION_KEY;
/** * zlf */@Slf4j@Activate(group = {CONSUMER, PROVIDER}, value = VALIDATION_KEY, order = -1)public class DubboValidationFilter implements Filter {
private Validation validation;
/** * Sets the validation instance for DubboValidationFilter * * @param validation Validation instance injected by dubbo framework based on "validation" attribute value. */ public void setValidation(Validation validation) { this.validation = validation; }
/** * Perform the validation of before invoking the actual method based on validation attribute value. * * @param invoker service * @param invocation invocation. * @return Method invocation result * @throws RpcException Throws RpcException if validation failed or any other runtime exception occurred. */ @Override public Result invoke(Invoker invoker, Invocation invocation) throws RpcException { if (validation != null && !invocation.getMethodName().startsWith("$") && ConfigUtils.isNotEmpty(invoker.getUrl().getMethodParameter(invocation.getMethodName(), VALIDATION_KEY))) { try { Validator validator = validation.getValidator(invoker.getUrl()); if (validator != null) { validator.validate(invocation.getMethodName(), invocation.getParameterTypes(), invocation.getArguments()); } } catch (RpcException e) { log.error("DubboValidationFilter.RpcException:{}", e.getMessage()); throw e; } catch (ValidationException e) { // only use exception's message to avoid potential serialization issue String message = e.getMessage(); if (e instanceof ConstraintViolationException) { ConstraintViolationException ex = (ConstraintViolationException) e; // 可能存在多个验证结果 因此组装返回 List<String> messageArray = new ArrayList<String>(); Set<ConstraintViolation> constraintViolations = ex.getConstraintViolations(); if (CollectionUtil.isNotEmpty(constraintViolations)) { constraintViolations.forEach(constraintViolation -> { messageArray.add(constraintViolation.getMessage()); }); } message = messageArray.size() == 1 ? messageArray.get(0) : String.join(",", messageArray); } //return AsyncRpcResult.newDefaultAsyncResult(new ValidationException(message), invocation); //这里的RestResponse是业务自己定义的一个响应的类,可以根据自己的需求去新建一个RestResponse类或这个是使用上面注释的代码放开即可,直接抛接口异常给调用方也是可以的 return AsyncRpcResult.newDefaultAsyncResult(RestResponse.fail(message), invocation); } catch (Throwable t) { log.error("DubboValidationFilter.Throwable:{}", t.getMessage()); return AsyncRpcResult.newDefaultAsyncResult(t, invocation); } } return invoker.invoke(invocation); }
}


2.4分组校验

ValidationGroups类如下:
public class ValidationGroups {
public interface Update { }
public interface Insert { }
public interface Detail {
}
public interface Delete{
}
}
dubbo接口api如下,dubbo接口参数分组校验需要加@MethodValidated注解,注解的value是一个数组里面可以写多个参数,使用@MethodValidated注解就可做分组校验,多个接口的参数实体可以公用一个类
import org.apache.dubbo.validation.MethodValidated;
import javax.validation.constraints.NotNull;
public interface xxxApi {
//新增接口只校验新增接口的参数 @MethodValidated(ValidationGroups.Insert.class) RestResponse add(@NotNull(message = "参数不为空!") xxxDto dto); //编辑接口校验插入接口的参数和编辑接口的参数 @MethodValidated({ValidationGroups.Insert.class, ValidationGroups.Update.class}) RestResponse edit(@NotNull(message = "参数不为空!") xxxDto dto);

}
xxxDto类如下:
package xxx.dto;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;import lombok.Data;
import javax.validation.constraints.NotBlank;import javax.validation.constraints.NotNull;import java.io.Serializable;
@Datapublic class xxxDto implements Serializable {
private static final long serialVersionUID = 2328308931670082723L;
/** * 主键 */ @NotNull(message = "id不为空", groups = {ValidationGroups.Update.class}) @JsonSerialize(using = ToStringSerializer.class) private Long id;
/** * 登录账号 */ @NotBlank(message = "账号不能为空", groups = {ValidationGroups.Insert.class, ValidationGroups.Update.class}) private String account;

}

dubbo接口实现类:
package xxx.service.dubbo;
import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.apache.dubbo.config.annotation.DubboService;import org.springframework.beans.factory.annotation.Value;
import java.util.Objects;
@Slf4j@DubboService(version = "${dubbo.application.version}", timeout = 3000, delay = -1, retries = 0, validation = "true", filter = "-validation,dubboValidationFilter")@RequiredArgsConstructorpublic class xxxApiImpl implements xxxApi {

@Override public RestResponse add(xxxDto dto) { ,,,,,,,,,,,,,,,,,,,,,, return RestResponse.success(); }
@Override public RestResponse edit(xxxDto dto) { ,,,,,,,,,,,,,,,,,,,,,,,, return RestResponse.success(); }
}


2.5嵌套校验

嵌套校验需要在xxxDto类的某个字段上加上@Valid注解,该注解的特性就是支持嵌套校验
@Valid
@NotEmpty(message = "List不为空", groups = {ValidationGroups.Insert.class, ValidationGroups.Update.class})
private List xDtos;

xxxx2Dto类的字段上也是加参数校验的注解的,实体上的字段都是可以使用validation的校验注解的,根据自己的需求来使用即可。


2.6校验生效的方式

2.6.1yaml配置

dubbo:  provider:    filter: -validation #排除自带的那个validationFilter,-表示排除    validation: true  # 服务提供者开启校验  consumer:    check: false    validation: true # 消费者开启校验  cloud:    subscribed-services: ''  scan:    base-packages: xxx.service.dubbo  protocol:    name: dubbo    port: -1  registry:    address: spring-cloud://localhost  application:    version: 1.0.0


2.6.2服务提供者接口配置

xxxApiImpl类上的配置:配置了validation = "true", filter = "-validation,dubboValidationFilter",排除自带的validationFilter和指定了上面自定义的dubboValidationFilter
@DubboService(version = "${dubbo.application.version}", timeout = 3000, delay = -1, retries = 0, validation = "true", filter = "-validation,dubboValidationFilter")@RequiredArgsConstructorpublic class xxxApiImpl implements xxxApi {}


2.6.3消费调用接口设置

@DubboReference里面设置validation等于true,开启注解校验
@DubboReference(version = "${dubbo.application.version}", validation = "true")private XxxxApi xxxxApi;

上面的几种方式推荐使用2.6.1yaml配置总体配置之后就不用在dubbo接口实现类的注解上加参数配置了,或者是单独在服务提供者接口实现类上加,或者是在接口消费调用的地方设置,2.6.2服务提供者接口配置和2.6.3消费调用接口设置这两种方式就是使用起来会有点麻烦,有dubbo接口实现类就要写几次,有几个调用的地方就要写几次,所以还是yaml全局设置后就不用在服务提供者或服务消费者调用的时候去加了。


3.总结

本文都是经过我亲测有效的,不像网上很多的坑文,千篇一律的,没有一个可以行的,使用这种方式就可以让接口优雅、干净、整洁、美观、工整,养成良好的编程习惯和代码风格,会减少很多bug的产生,更加专注于业务,而不至于由于参数校验有问题导致bug的出现,希望我的分享能给你有启发和帮助,请一键三连,么么么哒


请使用浏览器的分享功能分享到微信等