The background,

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

In the early days of HTTP interfaces provided for the front end, the verification of parameters may go through the following stages: write custom verification code for each interface and parameter, extract common verification logic, customize section verification, and universal standard verification logic.

The Validation logic of the generic standard mentioned here refers to the JSR303-based Java Bean Validation, where the official implementation is Hibernate Validator. It is very elegant to validate parameters in a Web project with Spring.

The main purpose of this article is to show you how to do elegant parameter verification with Dubbo.

Second, solutions

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

2.1 the maven rely on

<! Validation </groupId> <artifactId> Validation-api </artifactId> <version>2.01..Final</version> <! <scope>provided</scope> </dependency> <! <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>6.2. 0.Final</version>
</dependency>


Copy the code

2.2 Interface Definition

Facade interface definition:

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


Copy the code

Parameters are defined

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 blank ")
    private String name;
    @notblank (message = "user phone number cannot be blank ")
    @size (min = 8, Max = 16, message=" Phone number length between 8 and 16 digits ")
    private String phone;
 
    // getter and setter ignored
}


Copy the code

Common Return definition

/** * Facade interface returns the result */
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
}


Copy the code

2.3 Configuring the Dubbo Service Provider

Validation =”true” must be configured on the Dubbo service provider side as shown in the following example:

Dubbo interface server configuration

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


Copy the code

2.4 Dubbo service consumer configuration

Validation =”true” is not mandatory, but it is recommended that validation=”true” be configured as follows:

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


Copy the code

2.5 Verifying Parameters

After the previous steps are complete, the validation step is relatively simple. The consumer calls the convention interface, passing in the UpdateUserParam object with no field assignment, and then calls the server interface and gets the following parameter exception:

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's mobile phone number cannot be empty', propertyPath=phone, rootBeanClass=class com.xxx.demo.UpdateUserParam.messageTemplate='User's mobile phone number cannot be empty'}, ConstraintViolationImpl{interpolatedMessage='User ID cannot be null', propertyPath=id, rootBeanClass=class com.xxx.demo.UpdateUserParam.messageTemplate='User ID cannot be null'}]
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's mobile phone number cannot be empty', propertyPath=phone, rootBeanClass=class com.xxx.demo.UpdateUserParam.messageTemplate='User's mobile phone number cannot be empty'}, ConstraintViolationImpl{interpolatedMessage='User ID cannot be null', propertyPath=id, rootBeanClass=class com.xxx.demo.UpdateUserParam.messageTemplate='User ID cannot be null'}]
    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)


Copy the code

3. The customized Dubbo parameter verification is abnormal

When the Dubbo service is called by the consumer, if the parameter is invalid, it will throw an exception message, and the consumer will recognize the exception message when calling.

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 the correlation information will be prompted. Therefore, in cases where parameter validation is not valid, the service caller naturally does not want the server to throw a long exception containing stack information, but rather to maintain the uniform return form, as shown in the following return:

Dubbo interface server configuration:

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


Copy the code

3.1 ValidationFilter & JValidator

In order to make the return format consistent, let’s take a look at the previous exception thrown from how?

From the exception stack we can see that the exception message returned is thrown by ValidationFilter, which, as we can guess from its name, is a built-in implementation of Dubbo’s Filter extension mechanism. When validation=”true” is enabled on the Dubbo service interface, the Filter takes effect. 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 1validator.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) {
            returnAsyncRpcResult.newDefaultAsyncResult(t, invocation); }}return invoker.invoke(invocation);
}


Copy the code

As we can see from the exception stack above, the exception is generated by the ValidationException (valiator.validate) method at Note 1.

The Dubbo framework only implements the JValidator interface, as shown in the UML class diagram of the IDEA tool showing all implementations of the Validator (as shown below). Debugging code can also be easily located.

Now that we’ve located the JValidator, let’s take a look at the implementation of the validate method. The key code is as follows:

@Override
public void validate(String methodName, Class
       [] parameterTypes, Object[] arguments) throws Exception { List<Class<? >> groups =newArrayList<>(); Class<? > methodClass = methodClass(methodName);if(methodClass ! =null) { groups.add(methodClass); } Set<ConstraintViolation<? >> violations =newHashSet<>(); 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) {
        // 注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); }}Copy the code

It can be seen from the above codes that “violation of constraint” information obtained during parameter verification of codes “Note 1” and “Note 2” are added to the violations set, and when “violation of constraint” is not empty detected at “Note 3”, Will be thrown ConstraintViolationException containing information “” violates the constraint, since inherited the exception is thrown, it will be ValidationFilter captured in method, and then returned to the caller information related to abnormal.

3.2 Abnormal Verification of user-defined Parameters Is Returned

From the previous section, we can clearly understand why such an exception message is thrown to the caller. If we want to achieve what we want above: uniform return format, we need to follow the following steps to achieve it.

3.2.1 Customizing 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) {// This refines the exception type
                / / note 1Set<ConstraintViolation<? >> violations = e.getConstraintViolations();if(CollectionUtils.isNotEmpty(violations)) { ConstraintViolation<? > violation = violations.iterator().next();// select the first one
                    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) {
                returnAsyncRpcResult.newDefaultAsyncResult(t, invocation); }}returninvoker.invoke(invocation); }}Copy the code

The custom filter only different places with built-in ValidationFilter lies in “note 1” space new specific abnormal ConstraintViolationException processing, obtained from the exception object contains information of “constraint”, And take the first of these to construct the business-defined generic data format FacadeResult object as the information returned by the Dubbo service interface call.

3.2.2 Configuring a Custom Filter

Those of you who have developed the Dubbo custom filter know that to make it work you need to make a configuration that conforms to the SPI specification, as follows:

A. Create a meta-INF and dubbo directory. Do not create a meta-info. dubbo directory; otherwise, the initial startup fails.

B. Create a new file called com. Alibaba. Dubbo. RPC. The Filter, but it can be org. Apache. Dubbo. RPC. Filter, dubbo open source to the apache community, supported the two name by default.

C. is configured in the file content is: customValidationFilter = com. XXX. Demo. Dubbo. Filter. CustomValidationFilter.

3.3.3 Dubbo Service Configuration

There is also a problem with the Filter configuration of the custom parameter verification, if this is all you do, there will be two parameter verification filters after the application is started. Dubbo provides a mechanism to disable the specified Filter. The order of the Filter can be used to execute the custom Filter first. However, this method is not safe. Just do the following in the Dubbo configuration file:

<! -- To disable the filter"-"Start with the filter name --> <! Validation --> <dubbo:provider filter="-validation"/>


Copy the code

However, customValidationFilter does not take effect after the above configuration. After debugging and learning related documents of Dubbo, you have some understanding of the Filter taking effect mechanism.

A. After dubbo is started, a series of filters provided by the framework take effect by default.

Resource files in dubbo framework org. Apache. Dubbo. RPC. See what are the specific Filter, different versions of the content 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


Copy the code

The Filter in “Note 1” above is the Filter we want to disable in the previous configuration. Because these filters are built-in to Dubbo, these Filter sets have a unified name, default. Therefore, if you want to disable all of them, in addition to disabling them one by one, You can also use ‘-default’ directly to do this. These built-in filters work as long as they are not completely or individually disabled.

B. The custom Filter you want to develop can take effect, but it must not be reflected in <dubbo: Provider Filter =”xxxFitler” >; If we don’t have in Dubbo related configuration file to configure the Filter information, as long as write a custom Filter code, and in the resource file/meta-inf/Dubbo/com. Alibaba. Dubbo. RPC. The Filter can be defined according to the spi specification, Then all loaded filters will take effect.

C. If the Filter information is configured in the Dubbo configuration file, the user-defined Filter takes effect only when explicitly configured.

D. Filter configuration can also be added to dubbo service configuration (<dubbo:service interface=”…”). ref=”…” The validation = “true” filter = “xFilter, yFilter” / >).

If Filter information is configured for both provider and service in the Dubbo configuration file, the Filter that takes effect for service is the combination of the two parameters.

Therefore, to make the user-defined verification Filter take effect in all services, you need to perform the following configuration:

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


Copy the code

How to extend validation annotations

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

Consider a scenario where a parameter value needs to be validated only within a specified number of values, similar to whitelisting. Here is a demonstration of how to extend validation annotations.

4.1 Defining validation annotations

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { })/ / note 1
/ / @ the Constraint (validatedBy = {AllowedValueValidator. Class}) 2
public @interface AllowedValue {
 
    String message(a) default"Parameter value is not within legal range"; Class<? >[] groups()default{}; Class<? extends Payload>[] payload()default{};long[] value() default {};
 
}


Copy the code
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;
        }
        returnArrays.stream(allowedValues).anyMatch(o -> Objects.equals(o, value)); }}Copy the code

The Validator in Note 1 is not specified. It is certainly possible to specify validators directly as in Note 2, but it is not recommended to specify validators directly because custom annotations may be exposed directly in a facade package and the implementation of a Validator may sometimes contain business dependencies. Instead, the association is done through the Validator discovery mechanism provided by Hibernate Validator.

4.2 Configuring custom Validator Discovery

A. in the resources directory meta-inf/services/new javax.mail. Validation. ConstraintValidator file.

B. file only need to fill in the corresponding the full path of the Validator: com. XXX. Demo. The Validator. AllowedValueValidator, if there are multiple, one per line.

Five, the summary

This article mainly introduces how to use the Dubbo framework how to use elegant point to complete the verification of parameters, first demonstrates how to use the Dubbo framework default support verification implementation, then demonstrates how to use the actual business development to return a unified data format, and finally introduces how to customize the verification annotation implementation. It is convenient to carry on the follow-up self-extension realization, hoping to have certain help in the actual work.

Author: Wei Fuping, Development team of Vivo official website Mall