How does Dubbo service check parameters gracefully

Posted by barrywood on Tue, 08 Mar 2022 03:08:54 +0100

1, Background

When the server provides interface services to the outside, whether it is providing HTTP interfaces to the front end or RPC interfaces to other internal servers, it often faces such a problem, that is, how to gracefully solve the verification of various interface parameters?

In the early days, when making the HTTP interface provided for the front end, the verification of parameters may go through these stages: writing custom verification code for each parameter of each interface, refining public verification logic, verifying with custom aspects, and general standard verification logic.

The verification logic of the general standard mentioned here refers to the verification logic based on JSR303 Java Bean Validation , of which the specific implementation officially designated is Hibernate Validator , combining Spring in Web projects can do parameter verification gracefully.

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

2, Solution

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

2.1 maven dependency

<!-- Defined in facade Interface module pom The file is looking for that -->
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
<!-- If you don't want to facade The package has redundant dependencies, here scope Set as provided,Otherwise, it can be deleted -->
    <scope>provided</scope>
</dependency>
 
<!-- The following dependencies are usually added to Facade Interface implementation module pom In the file -->
<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 = "User ID cannot be empty")
    private Long id;
    @NotBlank(message = "User name cannot be empty")
    private String name;
    @NotBlank(message = "User mobile number cannot be empty")
    @Size(min = 8, max = 16, message="Phone number length between 8~16 position")
    private String phone;
 
    // getter and setter ignored
}

Public return definition

/**
 * Facade Interface unified return results
 */
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 configuration of Dubbo service provider

The Dubbo service provider must configure this validation="true". 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 consumer configuration of Dubbo service

This is not mandatory according to the usage habits of the business party, 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 a few steps, it is easier to verify this step. The consumer calls the agreed interface, and the interface enters the UpdateUserParam object, where the field is not assigned, and then calls the server interface to get the following parameter exceptions.

Dubbo interface server configuration

javax.validation.ValidationException: Failed to validate service: com.xxx.demo.UserFacade, method: updateUser, cause: [ConstraintViolationImpl{interpolatedMessage='User name cannot be empty', propertyPath=name, rootBeanClass=class com.xxx.demo.UpdateUserParam, messageTemplate='User name cannot be empty'}, ConstraintViolationImpl{interpolatedMessage='User mobile number cannot be empty', propertyPath=phone, rootBeanClass=class com.xxx.demo.UpdateUserParam, messageTemplate='User mobile number cannot be empty'}, ConstraintViolationImpl{interpolatedMessage='User ID cannot be empty', propertyPath=id, rootBeanClass=class com.xxx.demo.UpdateUserParam, messageTemplate='User ID cannot be empty'}]
javax.validation.ValidationException: Failed to validate service: com.xxx.demo.UserFacade, method: updateUser, cause: [ConstraintViolationImpl{interpolatedMessage='User name cannot be empty', propertyPath=name, rootBeanClass=class com.xxx.demo.UpdateUserParam, messageTemplate='User name cannot be empty'}, ConstraintViolationImpl{interpolatedMessage='User mobile number cannot be empty', propertyPath=phone, rootBeanClass=class com.xxx.demo.UpdateUserParam, messageTemplate='User mobile number cannot be empty'}, ConstraintViolationImpl{interpolatedMessage='User ID cannot be empty', propertyPath=id, rootBeanClass=class com.xxx.demo.UpdateUserParam, messageTemplate='User ID cannot be empty'}]
    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 exception return

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

However, from 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 prompted in combination with relevant information. Therefore, in case of illegal parameter verification, the service caller naturally does not want the server to throw a large section of exception information containing stack information, but wants to maintain this unified return form, as shown in the following return:

Dubbo interface server configuration:

{ 
  "code": 1001,
  "msg": "User name cannot be empty",
  "data": null
}

3.1 ValidationFilter & JValidator

To unify the return format, let's take a look at how the exception thrown earlier comes from?

From the contents of the exception stack, we can see that this exception information is returned by the ValidationFilter. From the name, we can guess that this is a built-in implementation using Dubbo's Filter extension mechanism. When we enable parameter verification on the Dubbo service interface (i.e. validation="true" in the previous Dubbo service configuration), the Filter will really work, Let's 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) {
                // Note 1
                validator.validate(invocation.getMethodName(), invocation.getParameterTypes(), invocation.getArguments());
            }
        } catch (RpcException e) {
            throw e;
        } catch (ValidationException e) {
            // Note 2
            return AsyncRpcResult.newDefaultAsyncResult(new ValidationException(e.getMessage()), invocation);
        } catch (Throwable t) {
            return AsyncRpcResult.newDefaultAsyncResult(t, invocation);
        }
    }
    return invoker.invoke(invocation);
}

From the above exception stack information, we can know that the exception information is generated by the above code "Note 2". This is because the ValidationException is caught. Through reading the code or debugging, we can know that the exception is generated by the validator at "note 1" Generated by the validate method.

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

Now that we have located the 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) {
        // Note 1
        violations.addAll(validator.validate(parameterBean, classgroups ));
    }
 
    for (Object arg : arguments) {
        // Note 2
        validate(violations, arg, classgroups);
    }
 
    if (!violations.isEmpty()) {
        // Note 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 constraint" obtained when the code at "note 1" and "Note 2" performs parameter verification is added to the violations set, and when it is checked that the "violation of constraint" is not empty at "note 3", a ConstraintViolationException containing the information of "violation of constraint" will be thrown, which is inherited from the ValidationException, This will also be captured by the method in ValidationFilter, and then return relevant exception information to the caller.

3.2 user defined parameter verification exception return

In the previous section, we can clearly understand why such exception information is thrown to the caller. If we want to achieve our previous demand: unified return format, we need to follow the following steps.

3.2.1 user defined 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) {// The exception type is refined here
                // Note 1
                Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
                if (CollectionUtils.isNotEmpty(violations)) {
                    ConstraintViolation<?> violation = violations.iterator().next();// Just take the first one to prompt
                    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 the custom filter and the built-in ValidationFilter lies in the processing of the specific exception ConstraintViolationException added in "note 1", which obtains the "constraint violation" information contained in the exception object, and takes the first one to construct the FacadeResult object of the common data format defined in the business, Information returned as a Dubbo service interface call.

3.2.2 user defined Filter configuration

Students who have developed Dubbo custom filter know that to make it effective, a configuration conforming to SPI specification needs to be made, as shown below:

a. The newly created two-level directories are META-INF and dubbo. Special attention should be paid to this. You cannot directly create a directory named "META-INFO.dubbo", otherwise it will fail during initialization and startup.

b. Create a new file named com alibaba. dubbo. rpc. Filter, of course, can also be org apache. dubbo. rpc. Filter and Dubbo support these two names by default after they are open-source to the Apache community.

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

3.3.3 Dubbo service configuration

After you have the Filter configuration of user-defined parameter verification, if you only do this, there is another problem. After the application is started, two parameter verification filters 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 repeated, so only one of them needs to take effect. Dubbo provides a mechanism to disable the specified Filter. Just make the following configuration in the Dubbo configuration file:

<!-- Need to disable filter with"-"Start with filter name -->
<!-- Check the source code and you can see what needs to be disabled ValidationFilter be known as validation-->
<dubbo:provider filter="-validation"/>

However, after the above configuration, it is found that the customValidationFilter does not take effect. After debugging and learning the relevant documents of dubbo, we have a certain understanding of the effective mechanism of the Filter.

a. After Dubbo is started, a series of filters provided by the framework will take effect by default;

The resource framework can be found at bbo.duo apache. dubbo. rpc. The contents of different versions may be slightly different.

cache=org.apache.dubbo.cache.filter.CacheFilter
validation=org.apache.dubbo.validation.filter.ValidationFilter  // Note 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 filters in the above "note 1" are the filters we want to disable in the previous configuration. Because these filters are built-in in Dubbo, these filter sets have a unified name, default. Therefore, if you want to disable all of them, in addition to one by one, you can also directly use '- default' to achieve the purpose. As long as all of these default built-in filters are not disabled or separately, That will take effect.

b. The user-defined filter that you want to develop can take effect, which does not have to be reflected in < Dubbo: provider filter = "xxxfitler" >; If we don't configure the filter related information in the Dubbo related configuration file, just write the custom filter code and put it in the resource file / meta-inf / Dubbo / com alibaba. dubbo. rpc. The filter can be defined according to the spi specification, so that all loaded filters will take effect.

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

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

When both the provider and service parts of the dubbo configuration file are configured with Filter information, the specific effective Filter for the service takes the union of the two configurations.

Therefore, if you want the custom verification Filter to take effect in all services, you need to make the following configuration:

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

4, How to extend verification annotation

In the previous examples, the built-in annotation of parameter verification is used. In actual development, sometimes the default built-in annotation cannot meet the verification requirements. At this time, some verification annotations need to be customized to meet the requirements for development convenience.

Suppose there is a scenario where a parameter value needs to be verified and can only be within a specified range of values, similar to the white list. Here is a scenario to demonstrate how to expand the verification annotation.

4.1 definition and verification notes

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { })// Note 1
// @Constraint(validatedBy = {AllowedValueValidator.class}) Note 2
public @interface AllowedValue {
 
    String message() default "The parameter value is not within the legal range";
 
    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 in "note 1" is not specified. Of course, the Validator can be specified directly as in "Note 2". However, considering that the custom annotation may be directly exposed in the facade package, and the implementation of the specific Validator sometimes contains some business dependencies, it is not recommended to specify it directly here, Instead, the association is completed through the Validator discovery mechanism provided by Hibernate Validator.

4.2 configuring custom Validator discovery

a. Create a new meta-inf / services / javax.xml file in the resources directory validation. Constraintvalidator file.

b. Just fill in the full path of the corresponding Validator in the file: com xxx. demo. Validator. Allowedvaluevalidator, if there are multiple, one for each line.

5, Summary

This paper mainly introduces how to use the elegant point method to complete the parameter verification when using the Dubbo framework. Firstly, it demonstrates how to use the verification implementation supported by the default of the Dubbo framework, then demonstrates how to cooperate with the actual business development to return the unified data format, and finally introduces how to implement the user-defined verification annotation, which is convenient for the subsequent self expansion implementation, I hope it can be of some help in practical work.

Author: Wei Fuping, development team of vivo official website mall

Topics: Java Dubbo server