Dubbo服务如何优雅的校验参数
⼀、背景
服务端在向外提供接⼝服务时,不管是对前端提供HTTP接⼝,还是⾯向内部其他服务端提供的RPC接⼝,常常会⾯对这样⼀个问题,就是如何优雅的解决各种接⼝参数校验问
题?
早期⼤家在做⾯向前端提供的HTTP接⼝时,对参数的校验可能都会经历这⼏个阶段:每个接⼝每个参数都写定制校验代码、提炼公共校验逻辑、⾃定义切⾯进⾏校验、通⽤标
准的校验逻辑。
这边提到的通⽤标准的校验逻辑指的就是基于JSR303的,其中官⽅指定的具体实现就是,在Web项⽬中结合Spring可以做到很优雅的去进⾏参数校验。
本⽂主要也是想给⼤家介绍下如何在使⽤Dubbo时做好优雅的参数校验。
⼆、解决⽅案
Dubbo框架本⾝是⽀持参数校验的,同时也是基于JSR303去实现的,我们来看下具体是怎么实现的。
2.1 maven依赖
<!-- 定义在facade接⼝模块的pom⽂件那个 -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
<!-- 如果不想facade包有多余的依赖,此处scope设为provided,否则可以删除 -->
<scope>provided</scope>
</dependency>
<!-- 下⾯依赖通常加在Facade接⼝实现模块的pom⽂件中 -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.0.Final</version>
</dependency>
2.2 接⼝定义
facade接⼝定义:
public interface UserFacade {
FacadeResult<Boolean> updateUser(UpdateUserParam param);
}
参数定义
public class UpdateUserParam implements Serializable {
private static final long serialVersionUID = 2476922055212727973L;
@NotNull(message = "⽤户标识不能为空")
private Long id;
@NotBlank(message = "⽤户名不能为空")
private String name;
@NotBlank(message = "⽤户⼿机号不能为空")
@Size(min = 8, max = 16, message="电话号码长度介于8~16位")
private String phone;
// getter and setter ignored
}
公共返回定义
/**
* Facade接⼝统⼀返回结果
*/
public class FacadeResult<T> implements Serializable {
private static final long serialVersionUID = 8570359747128577687L;
private int code;
private T data;
private String msg;
// getter and setter ignored
}
2.3 Dubbo服务提供者端配置
Dubbo服务提供者端必须作这个validation="true"的配置,具体⽰例配置如下:
Dubbo接⼝服务端配置
<bean class="demo.UserFacadeImpl" id="userFacade"/>
<dubbo:service interface="demo.UserFacade" ref="userFacade" validation="true" />
2.4 Dubbo服务消费者端配置
这个根据业务⽅使⽤习惯不作强制要求,但建议配置上都加上validation="true",⽰例配置如下:
<dubbo:reference id="userFacade" interface="demo.UserFacade" validation="true" />
2.5 验证参数校验
前⾯⼏步完成以后,验证这⼀步就⽐较简单了,消费者调⽤该约定接⼝,接⼝⼊参传⼊UpdateUserParam对象,其中字段不⽤赋值,然后调⽤服务端接⼝就会得到如下的参数异
常提⽰:
Dubbo接⼝服务端配置
javax.validation.ValidationException: Failed to validate service: demo.UserFacade, method: updateUser, cause: [ConstraintViolationImpl{interpolatedMessage='⽤户名不能为空', propertyPath=name, rootBeanClass=demo
javax.validation.ValidationException: Failed to validate service: demo.UserFacade, method: updateUser, cause: [ConstraintViolationImpl{interpolatedMessage='⽤户名不能为空', propertyPath=name, rootBeanClass=demo at org.apache.dubbo.validation.filter.ValidationFilter.invoke(ValidationFilter.java:96)
at org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:83)
....
at org.hange.support.ived(HeaderExchangeHandler.java:175)
at org.ived(DecodeHandler.java:51)
at org.ansport.dispatcher.ChannelEventRunnable.run(ChannelEventRunnable.java:57)
at urrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at urrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
三、定制Dubbo参数校验异常返回
从前⾯内容我们可以很轻松的验证,当消费端调⽤Dubbo服务时,参数如果不合法就会抛出相关异常信息,消费端调⽤时也能识别出异常信息,似乎这样就没有问题了。
但从前⾯所定义的服务接⼝来看,⼀般业务开发会定义统⼀的返回对象格式(如前⽂⽰例中的FacadeResult),对于业务异常情况,会约定相关异常码并结合相关性信息提⽰。
因此对于参数校验不合法的情况,服务调⽤⽅⾃然不希望服务端抛出⼀⼤段包含堆栈信息的异常信息,⽽是希望还保持这种统⼀的返回形式,就如下⾯这种返回所⽰:
Dubbo接⼝服务端配置:
{
"code": 1001,
"msg": "⽤户名不能为空",
"data": null
}
3.1 ValidationFilter & JValidator
想要做到返回格式的统⼀,我们先来看下前⾯所抛出的异常是如何来的?
从异常堆栈内容我们可以看出这个异常信息返回是由ValidationFilter抛出的,从名字我们可以猜到这个是采⽤Dubbo的Filter扩展机制的⼀个内置实现,当我们对Dubbo服务接⼝
启⽤参数校验时(即前⽂Dubbo服务配置中的validation="true"),该Filter就会真正起作⽤,我们来看下其中的关键实现逻辑:
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
if (validation != null && !MethodName().startsWith("$")
&& ConfigUtils.Url().MethodName(), VALIDATION_KEY))) {
try {
Validator validator = Url());
if (validator != null) {
// 注1
validator.MethodName(), ParameterTypes(), Arguments());
}
} catch (RpcException e) {
throw e;
} catch (ValidationException e) {
// 注2
wDefaultAsyncResult(new Message()), invocation);
} catch (Throwable t) {
wDefaultAsyncResult(t, invocation);
}
}
return invoker.invoke(invocation);
}
从前⽂的异常堆栈信息我们可以知道异常信息是由上述代码「注2」处所产⽣,这边是因为捕获了ValidationException,通过⾛读代码或者调试可以得知,该异常是由「注1」处
valiator.validate⽅法所产⽣。
⽽Validator接⼝在Dubbo框架中实现只有JValidator,这个通过idea⼯具显⽰Validator所有实现的UML类图可以看出(如下图所⽰),当然调试代码也可以很轻松定位到。
既然定位到JValidator了,我们就继续看下它⾥⾯validate⽅法的具体实现,关键代码如下所⽰:
@Override
public void validate(String methodName, Class<?>[] parameterTypes, Object[] arguments) throws Exception {
List<Class<?>> groups = new ArrayList<>();
Class<?> methodClass = methodClass(methodName);
if (methodClass != null) {
groups.add(methodClass);
}
Set<ConstraintViolation<?>> violations = new HashSet<>();
Method method = Method(methodName, parameterTypes);
Class<?>[] methodClasses;
if (method.isAnnotationPresent(MethodValidated.class)){
methodClasses = Annotation(MethodValidated.class).value();
groups.addAll(Arrays.asList(methodClasses));
}
groups.add(0, Default.class);
groups.add(1, clazz);
Class<?>[] classgroups = Array(new Class[groups.size()]);
Object parameterBean = getMethodParameterBean(clazz, method, arguments);
if (parameterBean != null) {
// 注1
violations.addAll(validator.validate(parameterBean, classgroups ));
}
for (Object arg : arguments) {
/
/ 注2
validate(violations, arg, classgroups);
}
if (!violations.isEmpty()) {
// 注3
<("Failed to validate service: " + Name() + ", method: " + methodName + ", cause: " + violations);
throw new ConstraintViolationException("Failed to validate service: " + Name() + ", method: " + methodName + ", cause: " + violations, violations);
}
}
从上述代码中可以看出当「注1」和注「2」两处代码进⾏参数校验时所得到的「违反约束」的信息都
被加⼊到violations集合中,⽽在「注3」处检查到「违反约束」不为空时,就会抛出包含「违反约束」信息的ConstraintViolationException,该异常继承⾃ValidationException,这样也就会被ValidationFilter中⽅法所捕获,进⽽向调⽤⽅返回相关异常信息。
3.2 ⾃定义参数校验异常返回
从前⼀⼩节我们可以很清晰的了解到了为什么会抛出那样的异常信息给调⽤⽅,如果想做到我们前⾯想要的诉求:统⼀返回格式,我们需要按照下⾯的步骤去实现。
3.2.1 ⾃定义Filter
@Activate(group = {CONSUMER, PROVIDER}, value = "customValidationFilter", order = 10000)
public class CustomValidationFilter implements Filter {
private Validation validation;
public void setValidation(Validation validation) { this.validation = validation; }
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
if (validation != null && !MethodName().startsWith("$")
&& ConfigUtils.Url().MethodName(), VALIDATION_KEY))) {
validation框架try {
Validator validator = Url());
if (validator != null) {
validator.MethodName(), ParameterTypes(), Arguments());
}
} catch (RpcException e) {
throw e;
} catch (ConstraintViolationException e) {// 这边细化了异常类型
// 注1
Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
if (CollectionUtils.isNotEmpty(violations)) {
ConstraintViolation<?> violation = violations.iterator().next();// 取第⼀个进⾏提⽰就⾏了
FacadeResult facadeResult = FacadeResult.fail(ErrorCode.Code(), Message());
wDefaultAsyncResult(facadeResult, invocation);
}
wDefaultAsyncResult(new Message()), invocation);
} catch (Throwable t) {
wDefaultAsyncResult(t, invocation);
}
}
return invoker.invoke(invocation);
}
}
该⾃定义filter与内置的ValidationFilter唯⼀不同的地⽅就在于「注1」处所新增的针对特定异常ConstraintViolationException的处理,从异常对象中获取包含的「违反约束」信息,并取其中第⼀个来构造业务上所定义的通⽤数据格式FacadeResult对象,作为Dubbo服务接⼝调⽤返回的信息。
3.2.2 ⾃定义Filter的配置
开发过Dubbo⾃定义filter的同学都知道,要让它⽣效需要作⼀个符合SPI规范的配置,如下所⽰:
a. 新建两级⽬录分别是META-INF和dubbo,这个需要特别注意,不能直接新建⼀个⽬录名为「META-INFO.dubbo」,否则在初始化启动的时候会失败。
b. 新建⼀个⽂件名为com.alibaba.dubbo.rp
c.Filter,当然也可以是org.apache.dubbo.rpc.Filter,Dubbo开源到Apache社区后,默认⽀持这两个名字。
c. ⽂件中配置内容为:demo.dubbo.filter.CustomValidationFilter。
3.3.3 Dubbo服务配置
有了⾃定义参数校验的Filter配置后,如果只做到这的话,其实还有⼀个问题,应⽤启动后会有两个参数校验Filter⽣效。当然可以通过指定Filter的order来实现⾃定义Filter先执⾏,但很显然这种⽅式不稳妥,⽽且两个Filter的功能是重复的,因此只需要⼀个⽣效就可以了,Dubbo提供了⼀种机制可以禁⽤指定的Filter,只需在Dubbo配置⽂件中作如下配置即可:
<!-- 需要禁⽤的filter以"-"开头并加上filter名称 -->
<!-- 查看源码,可看到需要禁⽤的ValidationFilter名为validation-->
<dubbo:provider filter="-validation"/>
但经过上述配置后,发现customValidationFilter并没有⽣效,经过调试以及对dubbo相关⽂档的学习,对Filter⽣效机制有了⼀定的了解。
a. dubbo启动后,默认会⽣效框架⾃带的⼀系列Filter;
可以在dubbo框架的资源⽂件org.apache.dubbo.rpc.Filter中看到具体有哪些,不同版本的内容可能会有些许差别。
cache=org.apache.dubbo.cache.filter.CacheFilter
validation=org.apache.dubbo.validation.filter.ValidationFilter // 注1
echo=org.apache.dubbo.rpc.filter.EchoFilter
generic=org.apache.dubbo.rpc.filter.GenericFilter
genericimpl=org.apache.dubbo.rpc.filter.GenericImplFilter
token=org.apache.dubbo.rpc.filter.TokenFilter
accesslog=org.apache.dubbo.rpc.filter.AccessLogFilter
activelimit=org.apache.dubbo.rpc.filter.ActiveLimitFilter
classloader=org.apache.dubbo.rpc.filter.ClassLoaderFilter
context=org.apache.dubbo.rpc.filter.ContextFilter
consumercontext=org.apache.dubbo.rpc.filter.ConsumerContextFilter
exception=org.apache.dubbo.rpc.filter.ExceptionFilter
executelimit=org.apache.dubbo.rpc.filter.ExecuteLimitFilter
deprecated=org.apache.dubbo.rpc.filter.DeprecatedFilter
compatible=org.apache.dubbo.rpc.filter.CompatibleFilter
timeout=org.apache.dubbo.rpc.filter.TimeoutFilter
tps=org.apache.dubbo.rpc.filter.TpsLimitFilter
trace=org.apache.dubbo.rpc.protocol.dubbo.filter.TraceFilter
future=org.apache.dubbo.rpc.protocol.dubbo.filter.FutureFilter
monitor=org.itor.support.MonitorFilter
metrics=org.itor.dubbo.MetricsFilter
如上「注1」中的Filter就是我们上⼀步配置中想要禁⽤的Filter,因为这些filter都是Dubbo内置的,所以这些filter集合有⼀个统⼀的名字,default,因此如果想全部禁⽤,除了⼀个⼀个禁⽤外,也可以直接⽤'-default'达到⽬的,这些默认内置的filter只要没有全部或单独禁⽤,那就会⽣效。
b. 想要开发的⾃定义Filter能⽣效,不并⼀定要在<dubbo:provider filter="xxxFitler" >中体现;如果我们没有在Dubbo相关的配置⽂件中去配置Filter相关信息,只要写好⾃定义filter代码,并在资源⽂件/META-INF/dubbo/com.alibaba.dubbo.rp
c.Filter中按照spi规范定义好即可,这样所有被加载的Filter都会⽣效。
c. 如果在Dubbo配置⽂件中配置了Filter信息,那⾃定义Filter只有显式配置才会⽣效。
d. Filter配置也可以加在dubbo service配置中(<dubbo:service interface="..." ref="..." validation="true" filter="xFilter,yFilter"/>)。
当dubbo配置⽂件中provider 和service部分都配置了Filter信息,针对service具体⽣效的Filter取两者配置的并集。
因此想要⾃定义的校验Filter在所有服务中都⽣效,需要作如下配置:
<dubbo:provider filter="-validation, customValidationFilter"/>
四、如何扩展校验注解
前⾯⽰例中都是利⽤参数校验的内置注解去完成,在实际开发中有时候会遇到默认内置的注解⽆法满⾜校验需求,这时就需要⾃定义⼀些校验注解去满⾜需求,⽅便开发。
假设有这样⼀个场景,某参数值需要校验只能在指定的⼏个数值范围内,类似于⽩名单⼀样,下⾯就以这个场景来演⽰下如何扩展校验注解。
4.1 定义校验注解
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { })// 注1
// @Constraint(validatedBy = {AllowedValueValidator.class}) 注2
public @interface AllowedValue {
String message() default "参数值不在合法范围内";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
long[] value() default {};
}
public class AllowedValueValidator implements ConstraintValidator<AllowedValue, Long> {
private long[] allowedValues;
@Override
public void initialize(AllowedValue constraintAnnotation) {
this.allowedValues = constraintAnnotation.value();
}
@Override
public boolean isValid(Long value, ConstraintValidatorContext context) {
if (allowedValues.length == 0) {
return true;
}
return Arrays.stream(allowedValues).anyMatch(o -> Objects.equals(o, value));
}
}
「注1」中的校验器(Validator)并没有指定,当然是可以像「注2」中那样直接指定校验器,但考虑到⾃定义注解有可能是直接暴露在facade包中,⽽具体的校验器的实现有时候会包含⼀些业务依赖,所以不建议直接在此处指定,⽽是通过Hibernate Validator提供的Validator发现机制去完成关联。
4.2 配置定制Validator发现
a. 在resources⽬录下新建META-INF/services/javax.validation.ConstraintValidator⽂件。
b. ⽂件中只需填⼊相应Validator的全路径:demo.validator.AllowedValueValidator,如果有多个的话,每⾏⼀个。
五、总结
本⽂主要介绍了使⽤Dubbo框架时如何使⽤优雅点⽅式完成参数的校验,⾸先演⽰了如何利⽤Dubbo框架默认⽀持的校验实现,然后接着演⽰了如何配合实际业务开发返回统⼀的数据格式,最后介绍了下如何进⾏⾃定义校验注解的实现,⽅便进⾏后续⾃⾏扩展实现,希望能在实际⼯作中有⼀定的帮助。
作者:vivo官⽹商城开发团队-Wei Fuping
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论