Dubbo服務(wù)校驗(yàn)參數(shù)的解決方案
本文分享了如何對Dubbo服務(wù)進(jìn)行優(yōu)雅的參數(shù)校驗(yàn),以實(shí)現(xiàn)服務(wù)端統(tǒng)一的數(shù)據(jù)返回格式,同時也在一定程度提升開發(fā)效率,避免重復(fù)簡單的參數(shù)校驗(yàn)邏輯.
一、背景
服務(wù)端在向外提供接口服務(wù)時,不管是對前端提供HTTP接口,還是面向內(nèi)部其他服務(wù)端提供的RPC接口,常常會面對這樣一個問題,就是如何優(yōu)雅的解決各種接口參數(shù)校驗(yàn)問題?
早期大家在做面向前端提供的HTTP接口時,對參數(shù)的校驗(yàn)可能都會經(jīng)歷這幾個階段:每個接口每個參數(shù)都寫定制校驗(yàn)代碼、提煉公共校驗(yàn)邏輯、自定義切面進(jìn)行校驗(yàn)、通用標(biāo)準(zhǔn)的校驗(yàn)邏輯。
這邊提到的通用標(biāo)準(zhǔn)的校驗(yàn)邏輯指的就是基于JSR303的Java Bean Validation,其中官方指定的具體實(shí)現(xiàn)就是 Hibernate Validator,在Web項(xiàng)目中結(jié)合Spring可以做到很優(yōu)雅的去進(jìn)行參數(shù)校驗(yàn)。
本文主要也是想給大家介紹下如何在使用Dubbo時做好優(yōu)雅的參數(shù)校驗(yàn)。
二、解決方案
Dubbo框架本身是支持參數(shù)校驗(yàn)的,同時也是基于JSR303去實(shí)現(xiàn)的,我們來看下具體是怎么實(shí)現(xiàn)的。
2.1 maven依賴
<!-- 定義在facade接口模塊的pom文件找那個 --> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.1.Final</version> <!-- 如果不想facade包有多余的依賴,此處scope設(shè)為provided,否則可以刪除 --> <scope>provided</scope> </dependency> <!-- 下面依賴通常加在Facade接口實(shí)現(xiàn)模塊的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); }
參數(shù)定義
public class UpdateUserParam implements Serializable { private static final long serialVersionUID = 2476922055212727973L; @NotNull(message = "用戶標(biāo)識不能為空") private Long id; @NotBlank(message = "用戶名不能為空") private String name; @NotBlank(message = "用戶手機(jī)號不能為空") @Size(min = 8, max = 16, message="電話號碼長度介于8~16位") private String phone; // getter and setter ignored }
公共返回定義
/** * Facade接口統(tǒng)一返回結(jié)果 */ 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服務(wù)提供者端配置
Dubbo服務(wù)提供者端必須作這個validation="true"的配置,具體示例配置如下:
Dubbo接口服務(wù)端配置
<bean class="com.xxx.demo.UserFacadeImpl" id="userFacade"/> <dubbo:service interface="com.xxx.demo.UserFacade" ref="userFacade" validation="true" />
2.4 Dubbo服務(wù)消費(fèi)者端配置
這個根據(jù)業(yè)務(wù)方使用習(xí)慣不作強(qiáng)制要求,但建議配置上都加上validation="true",示例配置如下:
<dubbo:reference id="userFacade" interface="com.xxx.demo.UserFacade" validation="true" />
2.5 驗(yàn)證參數(shù)校驗(yàn)
前面幾步完成以后,驗(yàn)證這一步就比較簡單了,消費(fèi)者調(diào)用該約定接口,接口入?yún)魅險pdateUserParam對象,其中字段不用賦值,然后調(diào)用服務(wù)端接口就會得到如下的參數(shù)異常提示:
Dubbo接口服務(wù)端配置
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='用戶手機(jī)號不能為空', propertyPath=phone, rootBeanClass=class com.xxx.demo.UpdateUserParam, messageTemplate='用戶手機(jī)號不能為空'}, ConstraintViolationImpl{interpolatedMessage='用戶標(biāo)識不能為空', propertyPath=id, rootBeanClass=class com.xxx.demo.UpdateUserParam, messageTemplate='用戶標(biāo)識不能為空'}] 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='用戶手機(jī)號不能為空', propertyPath=phone, rootBeanClass=class com.xxx.demo.UpdateUserParam, messageTemplate='用戶手機(jī)號不能為空'}, ConstraintViolationImpl{interpolatedMessage='用戶標(biāo)識不能為空', propertyPath=id, rootBeanClass=class com.xxx.demo.UpdateUserParam, messageTemplate='用戶標(biāo)識不能為空'}] 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參數(shù)校驗(yàn)異常返回
從前面內(nèi)容我們可以很輕松的驗(yàn)證,當(dāng)消費(fèi)端調(diào)用Dubbo服務(wù)時,參數(shù)如果不合法就會拋出相關(guān)異常信息,消費(fèi)端調(diào)用時也能識別出異常信息,似乎這樣就沒有問題了。
但從前面所定義的服務(wù)接口來看,一般業(yè)務(wù)開發(fā)會定義統(tǒng)一的返回對象格式(如前文示例中的FacadeResult),對于業(yè)務(wù)異常情況,會約定相關(guān)異常碼并結(jié)合相關(guān)性信息提示。因此對于參數(shù)校驗(yàn)不合法的情況,服務(wù)調(diào)用方自然不希望服務(wù)端拋出一大段包含堆棧信息的異常信息,而是希望還保持這種統(tǒng)一的返回形式,就如下面這種返回所示:
Dubbo接口服務(wù)端配置:
{ "code": 1001, "msg": "用戶名不能為空", "data": null }
3.1 ValidationFilter & JValidator
想要做到返回格式的統(tǒng)一,我們先來看下前面所拋出的異常是如何來的?
從異常堆棧內(nèi)容我們可以看出這個異常信息返回是由ValidationFilter拋出的,從名字我們可以猜到這個是采用Dubbo的Filter擴(kuò)展機(jī)制的一個內(nèi)置實(shí)現(xiàn),當(dāng)我們對Dubbo服務(wù)接口啟用參數(shù)校驗(yàn)時(即前文Dubbo服務(wù)配置中的validation="true"),該Filter就會真正起作用,我們來看下其中的關(guān)鍵實(shí)現(xiàn)邏輯:
@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); }
從前文的異常堆棧信息我們可以知道異常信息是由上述代碼「注2」處所產(chǎn)生,這邊是因?yàn)椴东@了ValidationException,通過走讀代碼或者調(diào)試可以得知,該異常是由「注1」處valiator.validate方法所產(chǎn)生。
而Validator接口在Dubbo框架中實(shí)現(xiàn)只有JValidator,這個通過idea工具顯示Validator所有實(shí)現(xiàn)的UML類圖可以看出(如下圖所示),當(dāng)然調(diào)試代碼也可以很輕松定位到。
既然定位到JValidator了,我們就繼續(xù)看下它里面validate方法的具體實(shí)現(xiàn),關(guān)鍵代碼如下所示:
@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); } }
從上述代碼中可以看出當(dāng)「注1」和注「2」兩處代碼進(jìn)行參數(shù)校驗(yàn)時所得到的「違反約束」的信息都被加入到violations集合中,而在「注3」處檢查到「違反約束」不為空時,就會拋出包含「違反約束」信息的ConstraintViolationException,該異常繼承自ValidationException,這樣也就會被ValidationFilter中方法所捕獲,進(jìn)而向調(diào)用方返回相關(guān)異常信息。
3.2 自定義參數(shù)校驗(yàn)異常返回
從前一小節(jié)我們可以很清晰的了解到了為什么會拋出那樣的異常信息給調(diào)用方,如果想做到我們前面想要的訴求:統(tǒng)一返回格式,我們需要按照下面的步驟去實(shí)現(xiàn)。
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) {// 這邊細(xì)化了異常類型 // 注1 Set<ConstraintViolation<?>> violations = e.getConstraintViolations(); if (CollectionUtils.isNotEmpty(violations)) { ConstraintViolation<?> violation = violations.iterator().next();// 取第一個進(jìn)行提示就行了 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與內(nèi)置的ValidationFilter唯一不同的地方就在于「注1」處所新增的針對特定異常ConstraintViolationException的處理,從異常對象中獲取包含的「違反約束」信息,并取其中第一個來構(gòu)造業(yè)務(wù)上所定義的通用數(shù)據(jù)格式FacadeResult對象,作為Dubbo服務(wù)接口調(diào)用返回的信息。
3.2.2 自定義Filter的配置
開發(fā)過Dubbo自定義filter的同學(xué)都知道,要讓它生效需要作一個符合SPI規(guī)范的配置,如下所示:
a. 新建兩級目錄分別是META-INF和dubbo,這個需要特別注意,不能直接新建一個目錄名為「META-INFO.dubbo」,否則在初始化啟動的時候會失敗。
b. 新建一個文件名為com.alibaba.dubbo.rpc.Filter,當(dāng)然也可以是org.apache.dubbo.rpc.Filter,Dubbo開源到Apache社區(qū)后,默認(rèn)支持這兩個名字。
c. 文件中配置內(nèi)容為:customValidationFilter=com.xxx.demo.dubbo.filter.CustomValidationFilter。
3.3.3 Dubbo服務(wù)配置
有了自定義參數(shù)校驗(yàn)的Filter配置后,如果只做到這的話,其實(shí)還有一個問題,應(yīng)用啟動后會有兩個參數(shù)校驗(yàn)Filter生效。當(dāng)然可以通過指定Filter的order來實(shí)現(xiàn)自定義Filter先執(zhí)行,但很顯然這種方式不穩(wěn)妥,而且兩個Filter的功能是重復(fù)的,因此只需要一個生效就可以了,Dubbo提供了一種機(jī)制可以禁用指定的Filter,只需在Dubbo配置文件中作如下配置即可:
<!-- 需要禁用的filter以"-"開頭并加上filter名稱 --> <!-- 查看源碼,可看到需要禁用的ValidationFilter名為validation--> <dubbo:provider filter="-validation"/>
但經(jīng)過上述配置后,發(fā)現(xiàn)customValidationFilter并沒有生效,經(jīng)過調(diào)試以及對dubbo相關(guān)文檔的學(xué)習(xí),對Filter生效機(jī)制有了一定的了解。
a. dubbo啟動后,默認(rèn)會生效框架自帶的一系列Filter;
可以在dubbo框架的資源文件org.apache.dubbo.rpc.Filter中看到具體有哪些,不同版本的內(nèi)容可能會有些許差別。
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
如上「注1」中的Filter就是我們上一步配置中想要禁用的Filter,因?yàn)檫@些filter都是Dubbo內(nèi)置的,所以這些filter集合有一個統(tǒng)一的名字,default,因此如果想全部禁用,除了一個一個禁用外,也可以直接用'-default'達(dá)到目的,這些默認(rèn)內(nèi)置的filter只要沒有全部或單獨(dú)禁用,那就會生效。
b. 想要開發(fā)的自定義Filter能生效,不并一定要在<dubbo:provider filter="xxxFitler" >中體現(xiàn);如果我們沒有在Dubbo相關(guān)的配置文件中去配置Filter相關(guān)信息,只要寫好自定義filter代碼,并在資源文件/META-INF/dubbo/com.alibaba.dubbo.rpc.Filter中按照spi規(guī)范定義好即可,這樣所有被加載的Filter都會生效。
c. 如果在Dubbo配置文件中配置了Filter信息,那自定義Filter只有顯式配置才會生效。
d. Filter配置也可以加在dubbo service配置中(<dubbo:service interface="..." ref="..." validation="true" filter="xFilter,yFilter"/>)。
當(dāng)dubbo配置文件中provider 和service部分都配置了Filter信息,針對service具體生效的Filter取兩者配置的并集。
因此想要自定義的校驗(yàn)Filter在所有服務(wù)中都生效,需要作如下配置:
<dubbo:provider filter="-validation, customValidationFilter"/>
四、如何擴(kuò)展校驗(yàn)注解
前面示例中都是利用參數(shù)校驗(yàn)的內(nèi)置注解去完成,在實(shí)際開發(fā)中有時候會遇到默認(rèn)內(nèi)置的注解無法滿足校驗(yàn)需求,這時就需要自定義一些校驗(yàn)注解去滿足需求,方便開發(fā)。
假設(shè)有這樣一個場景,某參數(shù)值需要校驗(yàn)只能在指定的幾個數(shù)值范圍內(nèi),類似于白名單一樣,下面就以這個場景來演示下如何擴(kuò)展校驗(yàn)注解。
4.1 定義校驗(yàn)注解
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER }) @Retention(RUNTIME) @Documented @Constraint(validatedBy = { })// 注1 // @Constraint(validatedBy = {AllowedValueValidator.class}) 注2 public @interface AllowedValue { String message() default "參數(shù)值不在合法范圍內(nèi)"; 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(); } public boolean isValid(Long value, ConstraintValidatorContext context) { if (allowedValues.length == 0) { return true; } return Arrays.stream(allowedValues).anyMatch(o -> Objects.equals(o, value)); }
「注1」中的校驗(yàn)器(Validator)并沒有指定,當(dāng)然是可以像「注2」中那樣直接指定校驗(yàn)器,但考慮到自定義注解有可能是直接暴露在facade包中,而具體的校驗(yàn)器的實(shí)現(xiàn)有時候會包含一些業(yè)務(wù)依賴,所以不建議直接在此處指定,而是通過Hibernate Validator提供的Validator發(fā)現(xiàn)機(jī)制去完成關(guān)聯(lián)。
4.2 配置定制Validator發(fā)現(xiàn)
a. 在resources目錄下新建META-INF/services/javax.validation.ConstraintValidator文件。
b. 文件中只需填入相應(yīng)Validator的全路徑:com.xxx.demo.validator.AllowedValueValidator,如果有多個的話,每行一個。
五、總結(jié)
本文主要介紹了使用Dubbo框架時如何使用優(yōu)雅點(diǎn)方式完成參數(shù)的校驗(yàn),首先演示了如何利用Dubbo框架默認(rèn)支持的校驗(yàn)實(shí)現(xiàn),然后接著演示了如何配合實(shí)際業(yè)務(wù)開發(fā)返回統(tǒng)一的數(shù)據(jù)格式,最后介紹了下如何進(jìn)行自定義校驗(yàn)注解的實(shí)現(xiàn),方便進(jìn)行后續(xù)自行擴(kuò)展實(shí)現(xiàn),希望能在實(shí)際工作中有一定的幫助。
相關(guān)文章
spring?eurake中使用IP注冊及問題小結(jié)
在開發(fā)spring?cloud的時候遇到一個很奇葩的問題,就是服務(wù)向spring?eureka中注冊實(shí)例的時候使用的是機(jī)器名,然后出現(xiàn)localhost、xxx.xx等這樣的內(nèi)容,這篇文章主要介紹了spring?eurake中使用IP注冊,需要的朋友可以參考下2023-07-07java判定數(shù)組或集合是否存在某個元素的實(shí)例
下面小編就為大家?guī)硪黄猨ava判定數(shù)組或集合是否存在某個元素的實(shí)例。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-01-01Spring Cloud Ribbon負(fù)載均衡器處理方法
這篇文章主要介紹了Spring Cloud Ribbon負(fù)載均衡器處理方法,看看是如何獲取服務(wù)實(shí)例,獲取以后做了哪些處理,處理后又是如何選取服務(wù)實(shí)例的,需要的朋友可以參考下2018-02-02SpringBoot自動配置實(shí)現(xiàn)流程詳細(xì)分析
這篇文章主要介紹了SpringBoot自動配置原理分析,SpringBoot是我們經(jīng)常使用的框架,那么你能不能針對SpringBoot實(shí)現(xiàn)自動配置做一個詳細(xì)的介紹。如果可以的話,能不能畫一下實(shí)現(xiàn)自動配置的流程圖。牽扯到哪些關(guān)鍵類,以及哪些關(guān)鍵點(diǎn)2022-12-12Java中BufferedReader類獲取輸入輸入字符串實(shí)例
這篇文章主要介紹了Java中BufferedReader類獲取輸入輸入字符串實(shí)例,分享了相關(guān)代碼示例,小編覺得還是挺不錯的,具有一定借鑒價值,需要的朋友可以參考下2018-02-02