看了同事寫(xiě)的代碼,我竟然開(kāi)始默默的模仿了。。。
背景
事情是這樣的,目前我正在參與 XXXX 項(xiàng)目的搭建,需要與第三方對(duì)接接口。在對(duì)方的接口中存在幾個(gè)異步通知,為了接口的安全性,需要對(duì)接口的參數(shù)進(jìn)行驗(yàn)簽處理。
為了方便大家對(duì)異步通知返回參數(shù)的處理,Z 同事提出要將該驗(yàn)簽功能進(jìn)行統(tǒng)一封裝,到時(shí)候大家只需要關(guān)注自己的業(yè)務(wù)邏輯即可。
Z同事的解決方案
Z 同事選擇的是“自定義參數(shù)解析器”的解決方案,接下來(lái)我們通過(guò)代碼來(lái)了解一下。
自定義注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public?@interface?RsaVerify?{
????
????/**
?????*?是否啟用驗(yàn)簽功能,默認(rèn)驗(yàn)簽
?????*/
????boolean?verifySign()?default?true;
}
自定義方法參數(shù)解析器
@AllArgsConstructor
@Component
//實(shí)現(xiàn)?HandlerMethodArgumentResolver?接口
public?class?RsaVerifyArgumentResolver?implements?HandlerMethodArgumentResolver?{
????private?final?SecurityService?securityService;
????/**
?????*?此方法用來(lái)判斷本次請(qǐng)求的接口是否需要解析參數(shù),
?????*?如果需要返回?true,然后調(diào)用下面的?resolveArgument?方法,
?????*??如果不需要返回?false
?????*/
????@Override
????public?boolean?supportsParameter(MethodParameter?parameter)?{
????????return?parameter.hasParameterAnnotation(RsaVerify.class);
????}
????/**
?????*?真正的解析方法,將請(qǐng)求中的參數(shù)值解析為某種對(duì)象
?????*?parameter?要解析的方法參數(shù)
?????*?mavContainer?當(dāng)前請(qǐng)求的?ModelAndViewContainer(為請(qǐng)求提供對(duì)模型的訪問(wèn))
?????*?webRequest?當(dāng)前請(qǐng)求
?????*?WebDataBinderFactory?用于創(chuàng)建?WebDataBinder?的工廠
?????*/
????@Override
????public?Object?resolveArgument(MethodParameter?parameter,?ModelAndViewContainer?mavContainer,?NativeWebRequest?webRequest,?WebDataBinderFactory?binderFactory)?throws?Exception?{
????????RsaVerify?parameterAnnotation?=?parameter.getParameterAnnotation(RsaVerify.class);
????????if?(!parameterAnnotation.verifySign())?{
????????????return?mavContainer.getModel();
????????}
????????
????????//對(duì)參數(shù)進(jìn)行處理并驗(yàn)簽的邏輯
????????......
????????
????????//返回處理后的實(shí)體類(lèi)參數(shù)
????????return?ObjectMapperFactory
????????????????.getDateTimeObjectMapper("yyyyMMddHHmmss")
????????????????.readValue(StringUtil.queryParamsToJson(sb.toString()),?parameter.getParameterType());
????}
???
}
創(chuàng)建配置類(lèi)
@Configuration
@AllArgsConstructor
public?class?PayTenantWebConfig?implements?WebMvcConfigurer?{
????private?final?RsaVerifyArgumentResolver?rsaVerifyArgumentResolver;
????
????/**
?????*?將自定義的方法參數(shù)解析器加入到配置類(lèi)中
?????*/
????@Override
????public?void?addArgumentResolvers(List?resolvers) ?{
????????resolvers.add(rsaVerifyArgumentResolver);
????}
}
使用
使用方法非常簡(jiǎn)單,只需要在參數(shù)上引入注解就可以了
@RestController
@Slf4j
@RequestMapping("/xxx")
public?class?XxxCallbackController?{
????/**
?????*?@param?params
?????*?@return
?????*/
????@PostMapping("/callback")
????public?String?callback(@RsaVerify?CallbackReq?params)?{
????????log.info("receive?callback?req={}",?params);
??//業(yè)務(wù)邏輯處理
??.....
??
????????return?"success";
????}
}
問(wèn)題
問(wèn)題一
看到這,細(xì)心的朋友應(yīng)該會(huì)有所疑問(wèn):既然這邊用到了自定義的注解,為什么不用切面來(lái)實(shí)現(xiàn),而是使用自定義的參數(shù)解析器呢?Very Good!這也是阿Q提出的疑問(wèn),同事說(shuō)是因?yàn)?jackson 的反序列化動(dòng)作優(yōu)先級(jí)遠(yuǎn)高于切面的優(yōu)先級(jí),所以還沒(méi)進(jìn)入切面就已經(jīng)報(bào)反序列化失敗的錯(cuò)誤了。
問(wèn)題二
為什么在 controller 中注解 @RequestBody 不見(jiàn)了?
要回答這個(gè)問(wèn)題,我們就得了解下HandlerMethodArgumentResolverComposite這個(gè)類(lèi)了,以下簡(jiǎn)稱(chēng)Composite。SpringMVC 在啟動(dòng)時(shí)會(huì)將所有的參數(shù)解析器放到 Composite 中,Composite 是所有參數(shù)的一個(gè)集合。當(dāng)對(duì)參數(shù)進(jìn)行解析時(shí)就會(huì)從該參數(shù)解析器集合中選擇一個(gè)支持對(duì) parameter 解析的參數(shù)解析器,然后使用該解析器進(jìn)行參數(shù)解析。
又因?yàn)?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">@RequestBody所以使用的參數(shù)解析器RequestResponseBodyMethodProcessor優(yōu)先級(jí)高于我們自定義的參數(shù)解析器,所以如果共用會(huì)被前者攔截解析,所以為了正常使用,我們需要將@RequestBody 注解去掉。
/**
?*?Find?a?registered?{@link?HandlerMethodArgumentResolver}?that?supports
?*?the?given?method?parameter.
?*/
@Nullable
private?HandlerMethodArgumentResolver?getArgumentResolver(MethodParameter?parameter)?{
????HandlerMethodArgumentResolver?result?=?this.argumentResolverCache.get(parameter);
????if?(result?==?null)?{
????????for?(HandlerMethodArgumentResolver?resolver?:?this.argumentResolvers)?{
????????????if?(resolver.supportsParameter(parameter))?{
????????????????result?=?resolver;
????????????????this.argumentResolverCache.put(parameter,?result);
????????????????break;
????????????}
????????}
????}
????return?result;
}
C同事的解決方案
上邊 Z 同事的方案已經(jīng)可以解決該問(wèn)題了,但是該方案還有兩個(gè)不足之處:
需要每一個(gè)回調(diào)都去創(chuàng)建自己的 controller層,沒(méi)有一個(gè)對(duì)外的統(tǒng)一入口;需要在方法上添加自定義注解,侵入性比較強(qiáng);
因此經(jīng)過(guò)我們的商議,決定摒棄該方案,但是該方案的思想值得我們學(xué)習(xí)。接下來(lái)讓我們分析一下新的解決方案:
定義業(yè)務(wù)接口類(lèi)
業(yè)務(wù)接口類(lèi)包含兩個(gè)方法:具體業(yè)務(wù)處理的類(lèi)型;業(yè)務(wù)的具體處理方法。
public?interface?INotifyService?{
?/**
??*?處理類(lèi)型
??*/
?public?String?handleType();
?/**
??*?處理具體業(yè)務(wù)
??*/
?Integer?handle(String?notifyBody);
}
異步通知統(tǒng)一入口
@AllArgsConstructor
@RestController
@RequestMapping(value?=?"/notify")
public?class?NotifyController?{
?private?IService?service;
????@PostMapping(value?=?"/receive")
????public?String?receive(@RequestBody?String?body)?{
????????//處理通知
????????Integer?status?=?service.handle(body);
????????return?"success";
????}
}
在 Iservice 中做兩個(gè)步驟:
在 spring 啟動(dòng)之后,收集所有的類(lèi)型為 INotifyService的類(lèi)并放入map中;將參數(shù)進(jìn)行處理轉(zhuǎn)化,并驗(yàn)簽處理;
private?ApplicationContext?applicationContext;
private?Map?notifyServiceMap;
/**
?*?啟動(dòng)加載
?*/
@PostConstruct
public?void?init(){
?Map?map?=?applicationContext.getBeansOfType(INotifyService.class);
?Collection?services?=?map.values();
?if(CollectionUtils.isEmpty(services)){
??return;
?}
?notifyServiceMap?=?services.stream().collect(Collectors.toMap(INotifyService::handleType,?x?->?x));
}
@Override
public?Map?getNotifyServiceMap()? {
?return?notifyServiceMap;
}
@Override
public?Integer?handle(String?body)?{
?//參數(shù)處理+驗(yàn)簽邏輯
????......
????????
?//獲取具體的業(yè)務(wù)實(shí)現(xiàn)類(lèi)
?INotifyService?notifyService=notifyServiceMap.get(notifyType);
?Integer?status=null;
?if(Objects.nonNull(notifyService))?{
??//執(zhí)行具體業(yè)務(wù)
??try?{
???status=notifyService.handle(JSON.toJSONString(requestParameter));
??}?catch?(Exception?e)?{
???e.printStackTrace();
??}
?}
?//后續(xù)邏輯處理
????......
????????
?return?status;
}
業(yè)務(wù)具體實(shí)現(xiàn)
@Service
public?class?NotifySignServiceImpl?implements?INotifyService?{
????@Override
????public?String?handleType()?{
????????return?"type_sign";
????}
????@Override
????@Transactional
????public?Integer?handle(String?notifyBody)?{
????????//具體的業(yè)務(wù)處理
????????......
????}
}
小結(jié)
此方案提供統(tǒng)一的異步通知入口,把公共的參數(shù)處理和驗(yàn)簽邏輯與業(yè)務(wù)邏輯剝離。 利用 java 動(dòng)態(tài)加載類(lèi)的特性,將實(shí)現(xiàn)類(lèi)通過(guò)類(lèi)型進(jìn)行收集。 利用 java 多態(tài)的特性,通過(guò)不同的實(shí)現(xiàn)類(lèi)來(lái)處理不同的業(yè)務(wù)邏輯。
看到這,相信大家已經(jīng)對(duì)這兩種實(shí)現(xiàn)方案有了一定的理解,大家可以試著在以后的項(xiàng)目中應(yīng)用一下,體驗(yàn)一把!
福利篇
最近我在掘金獲得了兩個(gè)小冊(cè)5折優(yōu)惠碼,不知道大家有沒(méi)有興趣。在本文下留言“說(shuō)出你對(duì)閱讀同事代碼的感受”即可參與活動(dòng),阿Q會(huì)從留言中選出兩位讀者免費(fèi)贈(zèng)送。

以上就是今天的全部?jī)?nèi)容了,如果你有不同的意見(jiàn)或者更好的idea,歡迎聯(lián)系阿Q,添加阿Q可以加入技術(shù)交流群參與討論呦!
文章風(fēng)格多變,配圖通俗易懂,故事生動(dòng)有趣。推薦幾篇文章,何不試著讀讀呢?
實(shí)戰(zhàn)篇:斷點(diǎn)續(xù)傳?文件秒傳?手?jǐn)]大文件上傳 故事篇:數(shù)據(jù)庫(kù)架構(gòu)演變之路 面試篇:虛擬機(jī)棧5連問(wèn)?一聽(tīng)心里就樂(lè)了 分享篇:InnoDB解決幻讀的方案 干貨篇:20張圖助你了解JVM運(yùn)行時(shí)數(shù)據(jù)區(qū),你還覺(jué)得枯燥嗎?
覺(jué)得還不錯(cuò)?記得一鍵四連呦??
