4

1. Background

When the server provides interface services to the outside world, whether it is an HTTP interface for the front end or an RPC interface for other internal servers, it often faces such a problem, that is, how to elegantly solve various interface parameter verification problems?

In the early days, when people made front-end HTTP interfaces, the verification of parameters might go through these stages: write custom verification code for each parameter of each interface, refine common verification logic, and customize aspects for verification. , Universal standard verification logic.

The verification logic of the general standard mentioned here refers to the Java Bean Validation based on JSR303, of which the officially designated specific implementation is Hibernate Validator , which can be elegantly parameterized in combination with Spring in Web projects check.

This article mainly wants to introduce to you how to do elegant parameter verification when using Dubbo.

2. Solutions

The Dubbo framework itself supports parameter verification, and is also implemented based on JSR303. Let's see how it is implemented.

2.1 maven dependencies

<!-- 定义在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 Interface Definition

facade interface definition:

public interface UserFacade {
    FacadeResult<Boolean> updateUser(UpdateUserParam param);
}

parameter definition

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
}

public return definition

/**
 * 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 service provider configuration

The Dubbo service provider must configure this validation="true" configuration. The specific example configuration is as follows:

Dubbo interface server configuration

<bean class="com.xxx.demo.UserFacadeImpl" id="userFacade"/>
<dubbo:service interface="com.xxx.demo.UserFacade" ref="userFacade" validation="true" />

2.4 Dubbo service consumer configuration

This is not mandatory according to the usage habits of the business side, but it is recommended to add validation="true" to the configuration. The example configuration is as follows:

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

2.5 Verification parameter verification

After the previous steps are completed, the verification step is relatively simple. The consumer calls the contract interface, and the interface input parameters are passed into the UpdateUserParam object, in which the fields do not need to be assigned, and then calling the server interface will get the following parameter exception prompt:

Dubbo interface server configuration

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)

3. Custom Dubbo parameter verification abnormal return

From the previous content, we can easily verify that when the consumer calls the Dubbo service, if the parameters are invalid, relevant exception information will be thrown, and the consumer can also identify the abnormal information when calling, it seems that there is no problem.

However, from the perspective of the service interface defined above, general business development will define a unified return object format (such as FacadeResult in the previous example). For business exceptions, relevant exception codes will be agreed and related information will be prompted. Therefore, in the case of invalid parameter verification, the service caller naturally does not want the server to throw a large section of exception information containing stack information, but hopes to maintain this unified return form, as shown in the following return :

Dubbo interface server configuration:

{ 
  "code": 1001,
  "msg": "用户名不能为空",
  "data": null
}

3.1 ValidationFilter & JValidator

To unify the return format, let's first look at how the exception thrown earlier came from?

From the content of the exception stack, we can see that this exception information return is thrown by ValidationFilter. From the name, we can guess that this is a built-in implementation of Dubbo's Filter extension mechanism. When we enable parameter verification for the Dubbo service interface ( That is, the validation="true" in the previous Dubbo service configuration), the Filter will really work, let's take a look at the key implementation logic:

@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) {
                // 注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);
}

From the exception stack information above, we can know that the exception information is generated by the above code "Note 2". This is because the ValidationException is caught. It can be known by walking through the code or debugging that the exception is generated by the "Note 1" at the valiator. Generated by the validate method.

The Validator interface is implemented in the Dubbo framework only by JValidator. This UML class diagram showing all the implementations of Validator through the idea tool can be seen (as shown in the figure below), of course, the debugging code can also be easily located.

Now that we have located JValidator, let's continue to look at the specific implementation of the validate method in it. The key code is as follows:

@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 = 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);
    }
}

It can be seen from the above code that the information of "violation of constraints" obtained when the two codes of "Note 1" and "Note 2" are used for parameter verification are added to the violations set, and checked at "Note 3" When "Constraint Violation" is not empty, ConstraintViolationException containing "Constraint Violation" information will be thrown, which inherits from ValidationException, which will also be caught by the method in ValidationFilter, and then return relevant exception information to the caller.

3.2 Custom parameter verification exception return

From the previous section, we can clearly understand why such exception information is thrown to the caller. If we want to achieve what we wanted earlier: unified return format, we need to follow the steps below to achieve it.

3.2.1 Custom 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);
    }
}

The only difference between this custom filter and the built-in ValidationFilter is the processing of the specific exception ConstraintViolationException added in "Note 1", the "Constraint Violation" information contained in the exception object is obtained, and the first one is used to construct The FacadeResult object of the general data format defined in the business is used as the information returned by the Dubbo service interface call.

3.2.2 Custom Filter configuration

Students who have developed Dubbo custom filters know that to make it effective, a configuration that conforms to the SPI specification is required, as shown below:

a. The new two-level directories are META-INF and dubbo. This requires special attention. You cannot directly create a new directory named "META-INFO.dubbo", otherwise it will fail during initialization.

b. Create a new file named com.alibaba.dubbo.rpc.Filter, of course, it can also be org.apache.dubbo.rpc.Filter. After Dubbo is open sourced to the Apache community, these two names are supported by default.

c. The configuration content in the file is: customValidationFilter=com.xxx.demo.dubbo.filter.CustomValidationFilter.

3.3.3 Dubbo service configuration

With the Filter configuration for custom parameter verification, if you only do this, there is actually another problem. After the application starts, there will be two parameters to verify that the Filter will take effect. Of course, the custom Filter can be executed first by specifying the order of the Filter, but obviously this method is not safe, and the functions of the two Filters are duplicated, so only one is required to take effect. Dubbo provides a mechanism to To disable the specified Filter, just do the following configuration in the Dubbo configuration file:

<!-- 需要禁用的filter以"-"开头并加上filter名称 -->
<!-- 查看源码,可看到需要禁用的ValidationFilter名为validation-->
<dubbo:provider filter="-validation"/>

But after the above configuration, it is found that the customValidationFilter does not take effect. After debugging and studying the relevant documents of dubbo, I have a certain understanding of the Filter validating mechanism.

a. After dubbo is started, a series of filters that come with the framework will take effect by default;

You can see the specifics in the resource file org.apache.dubbo.rpc.Filter of the dubbo framework. The content of different versions may be slightly different.
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.apache.dubbo.monitor.support.MonitorFilter
metrics=org.apache.dubbo.monitor.dubbo.MetricsFilter

The Filter in "Note 1" above is the Filter we want to disable in the previous configuration. Because these filters are built in Dubbo, these filter sets have a unified name, default, so if you want to disable them all, except one by one In addition to disabling, you can also use '-default' directly to achieve the purpose. As long as these default built-in filters are not disabled all or individually, they will take effect.

b. The custom Filter you want to develop does not have to be reflected in <dubbo:provider filter="xxxFitler" >; if we do not configure the Filter-related information in the Dubbo-related configuration file, just write the custom filter. Define the filter code and define it according to the spi specification in the resource file /META-INF/dubbo/com.alibaba.dubbo.rpc.Filter, so that all loaded Filters will take effect.

c. If Filter information is configured in the Dubbo configuration file, the custom Filter will only take effect if it is explicitly configured.

d. Filter configuration can also be added to the dubbo service configuration (<dubbo:service interface="..." ref="..." validation="true" filter="xFilter,yFilter"/>).

When the filter information is configured in both the provider and service parts of the dubbo configuration file, the filter that takes effect for the service is the union of the two configurations.

Therefore, if you want the custom verification filter to take effect in all services, you need to configure the following:

<dubbo:provider filter="-validation, customValidationFilter"/>

Fourth, how to extend the verification annotation

In the previous examples, the built-in annotations for parameter verification are used to complete. In actual development, sometimes the default built-in annotations cannot meet the verification requirements. In this case, you need to customize some verification annotations to meet the requirements and facilitate development. .

Suppose there is such a scenario, a parameter value needs to be verified only within a few specified value ranges, similar to the whitelist, the following will use this scenario to demonstrate how to extend the verification annotation.

4.1 Defining validation annotations

@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));
    }
}

The validator (Validator) in "Note 1" is not specified. Of course, the validator can be specified directly as in "Note 2", but considering that the custom annotation may be directly exposed in the facade package, and the specific The implementation of the validator sometimes contains some business dependencies, so it is not recommended to specify it directly here, but to complete the association through the Validator discovery mechanism provided by Hibernate Validator.

4.2 Configuring custom Validator discovery

a. Create a new META-INF/services/javax.validation.ConstraintValidator file in the resources directory.

b. Just fill in the full path of the corresponding Validator in the file: com.xxx.demo.validator.AllowedValueValidator, if there are multiple, one per line.

V. Summary

This article mainly introduces how to use the elegant method to complete the parameter verification when using the Dubbo framework. It first demonstrates how to use the verification implementation supported by the Dubbo framework by default, and then demonstrates how to cooperate with actual business development to return a unified data format. Finally, it introduces Here is how to implement custom verification annotations, which is convenient for subsequent self-expansion and implementation, and hope to be helpful in practical work.

Author: vivo official website mall development team - Wei Fuping

vivo互联网技术
3.3k 声望10.2k 粉丝