一、背景

服务端在向外提供接口服务时,不论是对前端提供HTTP接口,还是面向外部其余服务端提供的RPC接口,经常会面对这样一个问题,就是如何优雅的解决各种接口参数校验问题?

晚期大家在做面向前端提供的HTTP接口时,对参数的校验可能都会经验这几个阶段:每个接口每个参数都写定制校验代码、提炼公共校验逻辑、自定义切面进行校验、通用规范的校验逻辑。

这边提到的通用规范的校验逻辑指的就是基于JSR303的Java Bean Validation,其中官网指定的具体实现就是 Hibernate Validator,在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="com.xxx.demo.UserFacadeImpl" id="userFacade"/><dubbo:service interface="com.xxx.demo.UserFacade" ref="userFacade" validation="true" />

2.4 Dubbo服务消费者端配置

这个依据业务方应用习惯不作强制要求,但倡议配置上都加上validation="true",示例配置如下:

<dubbo:reference id="userFacade" interface="com.xxx.demo.UserFacade" validation="true" />

2.5 验证参数校验

后面几步实现当前,验证这一步就比较简单了,消费者调用该约定接口,接口入参传入UpdateUserParam对象,其中字段不必赋值,而后调用服务端接口就会失去如下的参数异样提醒:

Dubbo接口服务端配置

javax.validation.ValidationException: Failed to validate service: com.xxx.demo.UserFacade, method: updateUser, cause: [ConstraintViolationImpl{interpolatedMessage='用户名不能为空', propertyPath=name, rootBeanClass=class com.xxx.demo.UpdateUserParam, messageTemplate='用户名不能为空'}, ConstraintViolationImpl{interpolatedMessage='用户手机号不能为空', propertyPath=phone, rootBeanClass=class com.xxx.demo.UpdateUserParam, messageTemplate='用户手机号不能为空'}, ConstraintViolationImpl{interpolatedMessage='用户标识不能为空', propertyPath=id, rootBeanClass=class com.xxx.demo.UpdateUserParam, messageTemplate='用户标识不能为空'}]javax.validation.ValidationException: Failed to validate service: com.xxx.demo.UserFacade, method: updateUser, cause: [ConstraintViolationImpl{interpolatedMessage='用户名不能为空', propertyPath=name, rootBeanClass=class com.xxx.demo.UpdateUserParam, messageTemplate='用户名不能为空'}, ConstraintViolationImpl{interpolatedMessage='用户手机号不能为空', propertyPath=phone, rootBeanClass=class com.xxx.demo.UpdateUserParam, messageTemplate='用户手机号不能为空'}, ConstraintViolationImpl{interpolatedMessage='用户标识不能为空', propertyPath=id, rootBeanClass=class com.xxx.demo.UpdateUserParam, messageTemplate='用户标识不能为空'}]    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.apache.dubbo.remoting.exchange.support.header.HeaderExchangeHandler.received(HeaderExchangeHandler.java:175)    at org.apache.dubbo.remoting.transport.DecodeHandler.received(DecodeHandler.java:51)    at org.apache.dubbo.remoting.transport.dispatcher.ChannelEventRunnable.run(ChannelEventRunnable.java:57)    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)    at java.util.concurrent.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就会真正起作用,咱们来看下其中的要害实现逻辑:

@Overridepublic 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) {                // 注1                validator.validate(invocation.getMethodName(), invocation.getParameterTypes(), invocation.getArguments());            }        } catch (RpcException e) {            throw e;        } catch (ValidationException e) {            // 注2            return AsyncRpcResult.newDefaultAsyncResult(new ValidationException(e.getMessage()), invocation);        } catch (Throwable t) {            return AsyncRpcResult.newDefaultAsyncResult(t, invocation);        }    }    return invoker.invoke(invocation);}

从前文的异样堆栈信息咱们能够晓得异样信息是由上述代码「注2」处所产生,这边是因为捕捉了ValidationException,通过走读代码或者调试能够得悉,该异样是由「注1」处valiator.validate办法所产生。

而Validator接口在Dubbo框架中实现只有JValidator,这个通过idea工具显示Validator所有实现的UML类图能够看出(如下图所示),当然调试代码也能够很轻松定位到。

既然定位到JValidator了,咱们就持续看下它外面validate办法的具体实现,要害代码如下所示:

@Overridepublic 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 = clazz.getMethod(methodName, parameterTypes);    Class<?>[] methodClasses;    if (method.isAnnotationPresent(MethodValidated.class)){        methodClasses = method.getAnnotation(MethodValidated.class).value();        groups.addAll(Arrays.asList(methodClasses));    }    groups.add(0, Default.class);    groups.add(1, clazz);     Class<?>[] classgroups = groups.toArray(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        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);    }}

从上述代码中能够看出当「注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 && !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) {                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.INVALID_PARAM.getCode(), violation.getMessage());                    return AsyncRpcResult.newDefaultAsyncResult(facadeResult, invocation);                }                return AsyncRpcResult.newDefaultAsyncResult(new ValidationException(e.getMessage()), invocation);            } catch (Throwable t) {                return AsyncRpcResult.newDefaultAsyncResult(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.rpc.Filter,当然也能够是org.apache.dubbo.rpc.Filter,Dubbo开源到Apache社区后,默认反对这两个名字。

c. 文件中配置内容为:customValidationFilter=com.xxx.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.CacheFiltervalidation=org.apache.dubbo.validation.filter.ValidationFilter  // 注1echo=org.apache.dubbo.rpc.filter.EchoFiltergeneric=org.apache.dubbo.rpc.filter.GenericFiltergenericimpl=org.apache.dubbo.rpc.filter.GenericImplFiltertoken=org.apache.dubbo.rpc.filter.TokenFilteraccesslog=org.apache.dubbo.rpc.filter.AccessLogFilteractivelimit=org.apache.dubbo.rpc.filter.ActiveLimitFilterclassloader=org.apache.dubbo.rpc.filter.ClassLoaderFiltercontext=org.apache.dubbo.rpc.filter.ContextFilterconsumercontext=org.apache.dubbo.rpc.filter.ConsumerContextFilterexception=org.apache.dubbo.rpc.filter.ExceptionFilterexecutelimit=org.apache.dubbo.rpc.filter.ExecuteLimitFilterdeprecated=org.apache.dubbo.rpc.filter.DeprecatedFiltercompatible=org.apache.dubbo.rpc.filter.CompatibleFiltertimeout=org.apache.dubbo.rpc.filter.TimeoutFiltertps=org.apache.dubbo.rpc.filter.TpsLimitFiltertrace=org.apache.dubbo.rpc.protocol.dubbo.filter.TraceFilterfuture=org.apache.dubbo.rpc.protocol.dubbo.filter.FutureFiltermonitor=org.apache.dubbo.monitor.support.MonitorFiltermetrics=org.apache.dubbo.monitor.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.rpc.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}) 注2public @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的全门路:com.xxx.demo.validator.AllowedValueValidator,如果有多个的话,每行一个。

五、总结

本文次要介绍了应用Dubbo框架时如何应用优雅点形式实现参数的校验,首先演示了如何利用Dubbo框架默认反对的校验实现,而后接着演示了如何配合理论业务开发返回对立的数据格式,最初介绍了下如何进行自定义校验注解的实现,不便进行后续自行扩大实现,心愿能在理论工作中有肯定的帮忙。

作者:vivo官网商城开发团队-Wei Fuping