Dubbo接口参数校验的正确姿势
文章目录
1.前言
由于之前的文章分享了如下这篇文章:
@Validated或@Valid参数注解校验、自定义手机号注解检验及优雅统一异常处理
https://mp.weixin.qq.com/s/o3SqNXO8BFEq2YsrlcEixA
https://blog.csdn.net/qq_34905631/article/details/137821646?spm=1001.2014.3001.5501
后面在项目中使用dubbo接口,就想能不能让dubbo接口也能参数校验,那么就不用在代码中写很多的if/else的参数校验了,直接dubbo的参数校验就给做了,那这种接口代码是不是又优雅、干净整洁了很多,于是乎我就上网开始看了一个些千篇一律的文章,没有一篇可以的,然后就经过不断的尝试之后,dubbo接口参数校验的正确姿势还是被我搞出来了,下面就分享给大家。
2.代码示例
2.1版本
dubbo版本2.7.13,至于项目中如何整合dubbo之前的文章中都有分享或者可以去dubbo官网查看.
在dubbo2.7.x版本duboo开始进入Apache,所以之前下面自定义的注解filter的名字是如下的名字:
org.apache.dubbo.rpc.Filter
不在是之前alibaba那种命名了,这里需要特别注意。
2.2校验依赖
在dubbo的api接口的项目pom中引入校验依赖跟上面之前分享的文章的依赖可以说是一样
方式一;
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.3.12.RELEASE</version>
</dependency>
方式二;
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.0.Final</version>
</dependency>
方式一和方式二任选一种引入依赖即可。
2.3自定义DubboValidationFilter
在resources下新建一个目录:META-INF.dubbo下面新建一个如下文件;
org.apache.dubbo.rpc.Filter
文件内容如下;
dubboValidationFilter=xx.xx.xxxx.config.DubboValidationFilter
DubboValidationFilter类如下:
package xxxx.config;
import cn.hutool.core.collection.CollectionUtil;
import xxxx.RestResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.common.utils.ConfigUtils;
import org.apache.dubbo.rpc.AsyncRpcResult;
import org.apache.dubbo.rpc.Filter;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.Result;
import org.apache.dubbo.rpc.RpcException;
import org.apache.dubbo.validation.Validation;
import org.apache.dubbo.validation.Validator;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import static org.apache.dubbo.common.constants.CommonConstants.CONSUMER;
import static org.apache.dubbo.common.constants.CommonConstants.PROVIDER;
import static org.apache.dubbo.common.constants.FilterConstants.VALIDATION_KEY;
/**
* zlf
*/
@Slf4j
@Activate(group = {CONSUMER, PROVIDER}, value = VALIDATION_KEY, order = -1)
public class DubboValidationFilter implements Filter {
private Validation validation;
/**
* Sets the validation instance for DubboValidationFilter
*
* @param validation Validation instance injected by dubbo framework based on "validation" attribute value.
*/
public void setValidation(Validation validation) {
this.validation = validation;
}
/**
* Perform the validation of before invoking the actual method based on <b>validation</b> attribute value.
*
* @param invoker service
* @param invocation invocation.
* @return Method invocation result
* @throws RpcException Throws RpcException if validation failed or any other runtime exception occurred.
*/
@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) {
validator.validate(invocation.getMethodName(), invocation.getParameterTypes(), invocation.getArguments());
}
} catch (RpcException e) {
log.error("DubboValidationFilter.RpcException:{}", e.getMessage());
throw e;
} catch (ValidationException e) {
// only use exception's message to avoid potential serialization issue
String message = e.getMessage();
if (e instanceof ConstraintViolationException) {
ConstraintViolationException ex = (ConstraintViolationException) e;
// 可能存在多个验证结果 因此组装返回
List<String> messageArray = new ArrayList<String>();
Set<ConstraintViolation<?>> constraintViolations = ex.getConstraintViolations();
if (CollectionUtil.isNotEmpty(constraintViolations)) {
constraintViolations.forEach(constraintViolation -> {
messageArray.add(constraintViolation.getMessage());
});
}
message = messageArray.size() == 1 ? messageArray.get(0) : String.join(",", messageArray);
}
//return AsyncRpcResult.newDefaultAsyncResult(new ValidationException(message), invocation);
//这里的RestResponse是业务自己定义的一个响应的类,可以根据自己的需求去新建一个RestResponse类或这个是使用上面注释的代码放开即可,直接抛接口异常给调用方也是可以的
return AsyncRpcResult.newDefaultAsyncResult(RestResponse.fail(message), invocation);
} catch (Throwable t) {
log.error("DubboValidationFilter.Throwable:{}", t.getMessage());
return AsyncRpcResult.newDefaultAsyncResult(t, invocation);
}
}
return invoker.invoke(invocation);
}
}
2.4分组校验
ValidationGroups类如下:
public class ValidationGroups {
public interface Update {
}
public interface Insert {
}
public interface Detail {
}
public interface Delete{
}
}
dubbo接口api如下,dubbo接口参数分组校验需要加@MethodValidated注解,注解的value是一个数组里面可以写多个参数,使用@MethodValidated注解就可做分组校验,多个接口的参数实体可以公用一个类
import org.apache.dubbo.validation.MethodValidated;
import javax.validation.constraints.NotNull;
public interface xxxApi {
//新增接口只校验新增接口的参数
@MethodValidated(ValidationGroups.Insert.class)
RestResponse add(@NotNull(message = "参数不为空!") xxxDto dto);
//编辑接口校验插入接口的参数和编辑接口的参数
@MethodValidated({ValidationGroups.Insert.class, ValidationGroups.Update.class})
RestResponse edit(@NotNull(message = "参数不为空!") xxxDto dto);
}
xxxDto类如下:
package xxx.dto;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
@Data
public class xxxDto implements Serializable {
private static final long serialVersionUID = 2328308931670082723L;
/**
* 主键
*/
@NotNull(message = "id不为空", groups = {ValidationGroups.Update.class})
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
/**
* 登录账号
*/
@NotBlank(message = "账号不能为空", groups = {ValidationGroups.Insert.class, ValidationGroups.Update.class})
private String account;
}
dubbo接口实现类:
package xxx.service.dubbo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Value;
import java.util.Objects;
@Slf4j
@DubboService(version = "${dubbo.application.version}", timeout = 3000, delay = -1, retries = 0, validation = "true", filter = "-validation,dubboValidationFilter")
@RequiredArgsConstructor
public class xxxApiImpl implements xxxApi {
@Override
public RestResponse add(xxxDto dto) {
,,,,,,,,,,,,,,,,,,,,,,
return RestResponse.success();
}
@Override
public RestResponse edit(xxxDto dto) {
,,,,,,,,,,,,,,,,,,,,,,,,
return RestResponse.success();
}
}
2.5嵌套校验
嵌套校验需要在xxxDto类的某个字段上加上@Valid注解,该注解的特性就是支持嵌套校验
@Valid
@NotEmpty(message = "List不为空", groups = {ValidationGroups.Insert.class, ValidationGroups.Update.class})
private List<xxxx2Dto> xDtos;
xxxx2Dto类的字段上也是加参数校验的注解的,实体上的字段都是可以使用validation的校验注解的,根据自己的需求来使用即可。
2.6校验生效的方式
2.6.1yaml配置
dubbo:
provider:
filter: -validation #排除自带的那个validationFilter,-表示排除
validation: true # 服务提供者开启校验
consumer:
check: false
validation: true # 消费者开启校验
cloud:
subscribed-services: ''
scan:
base-packages: xxx.service.dubbo
protocol:
name: dubbo
port: -1
registry:
address: spring-cloud://localhost
application:
version: 1.0.0
2.6.2服务提供者接口配置
xxxApiImpl类上的配置:配置了validation = “true”, filter = “-validation,dubboValidationFilter”,排除自带的validationFilter和指定了上面自定义的dubboValidationFilter
@DubboService(version = "${dubbo.application.version}", timeout = 3000, delay = -1, retries = 0, validation = "true", filter = "-validation,dubboValidationFilter")
@RequiredArgsConstructor
public class xxxApiImpl implements xxxApi {
}
2.6.3消费调用接口设置
@DubboReference里面设置validation等于true,开启注解校验
@DubboReference(version = "${dubbo.application.version}", validation = "true")
private XxxxApi xxxxApi;
上面的几种方式推荐使用2.6.1yaml配置总体配置之后就不用在dubbo接口实现类的注解上加参数配置了,或者是单独在服务提供者接口实现类上加,或者是在接口消费调用的地方设置,2.6.2服务提供者接口配置和2.6.3消费调用接口设置这两种方式就是使用起来会有点麻烦,有dubbo接口实现类就要写几次,有几个调用的地方就要写几次,所以还是yaml全局设置后就不用在服务提供者或服务消费者调用的时候去加了。
3.总结
本文都是经过我亲测有效的,不像网上很多的坑文,千篇一律的,没有一个可以行的,使用这种方式就可以让接口优雅、干净、整洁、美观、工整,养成良好的编程习惯和代码风格,会减少很多bug的产生,更加专注于业务,而不至于由于参数校验有问题导致bug的出现,希望我的分享能给你有启发和帮助,请一键三连,么么么哒!