看了同事寫的代碼,我竟然開始默默的模仿了。。。

源?/?? ? ? ??文/?
背景
事情是這樣的,目前我正在參與 XXXX 項目的搭建,需要與第三方對接接口。在對方的接口中存在幾個異步通知,為了接口的安全性,需要對接口的參數(shù)進行驗簽處理。
為了方便大家對異步通知返回參數(shù)的處理,Z 同事提出要將該驗簽功能進行統(tǒng)一封裝,到時候大家只需要關注自己的業(yè)務邏輯即可。
Z同事的解決方案
Z 同事選擇的是“自定義參數(shù)解析器”的解決方案,接下來我們通過代碼來了解一下。
自定義注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public?@interface?RsaVerify?{
????
????/**
?????*?是否啟用驗簽功能,默認驗簽
?????*/
????boolean?verifySign()?default?true;
}
自定義方法參數(shù)解析器
@AllArgsConstructor
@Component
//實現(xiàn)?HandlerMethodArgumentResolver?接口
public?class?RsaVerifyArgumentResolver?implements?HandlerMethodArgumentResolver?{
????private?final?SecurityService?securityService;
????/**
?????*?此方法用來判斷本次請求的接口是否需要解析參數(shù),
?????*?如果需要返回?true,然后調用下面的?resolveArgument?方法,
?????*??如果不需要返回?false
?????*/
????@Override
????public?boolean?supportsParameter(MethodParameter?parameter)?{
????????return?parameter.hasParameterAnnotation(RsaVerify.class);
????}
????/**
?????*?真正的解析方法,將請求中的參數(shù)值解析為某種對象
?????*?parameter?要解析的方法參數(shù)
?????*?mavContainer?當前請求的?ModelAndViewContainer(為請求提供對模型的訪問)
?????*?webRequest?當前請求
?????*?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();
????????}
????????
????????//對參數(shù)進行處理并驗簽的邏輯
????????......
????????
????????//返回處理后的實體類參數(shù)
????????return?ObjectMapperFactory
????????????????.getDateTimeObjectMapper("yyyyMMddHHmmss")
????????????????.readValue(StringUtil.queryParamsToJson(sb.toString()),?parameter.getParameterType());
????}
???
}
創(chuàng)建配置類
@Configuration
@AllArgsConstructor
public?class?PayTenantWebConfig?implements?WebMvcConfigurer?{
????private?final?RsaVerifyArgumentResolver?rsaVerifyArgumentResolver;
????
????/**
?????*?將自定義的方法參數(shù)解析器加入到配置類中
?????*/
????@Override
????public?void?addArgumentResolvers(List?resolvers) ?{
????????resolvers.add(rsaVerifyArgumentResolver);
????}
}
使用
使用方法非常簡單,只需要在參數(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è)務邏輯處理
??.....
??
????????return?"success";
????}
}
問題
問題一
看到這,細心的朋友應該會有所疑問:既然這邊用到了自定義的注解,為什么不用切面來實現(xiàn),而是使用自定義的參數(shù)解析器呢?Very Good!這也是阿Q提出的疑問,同事說是因為?jackson?的反序列化動作優(yōu)先級遠高于切面的優(yōu)先級,所以還沒進入切面就已經(jīng)報反序列化失敗的錯誤了。
問題二
為什么在?controller?中注解?@RequestBody?不見了?
要回答這個問題,我們就得了解下HandlerMethodArgumentResolverComposite這個類了,以下簡稱Composite。SpringMVC?在啟動時會將所有的參數(shù)解析器放到?Composite?中,Composite?是所有參數(shù)的一個集合。當對參數(shù)進行解析時就會從該參數(shù)解析器集合中選擇一個支持對?parameter?解析的參數(shù)解析器,然后使用該解析器進行參數(shù)解析。
又因為@RequestBody所以使用的參數(shù)解析器RequestResponseBodyMethodProcessor優(yōu)先級高于我們自定義的參數(shù)解析器,所以如果共用會被前者攔截解析,所以為了正常使用,我們需要將@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)可以解決該問題了,但是該方案還有兩個不足之處:
需要每一個回調都去創(chuàng)建自己的? controller?層,沒有一個對外的統(tǒng)一入口;需要在方法上添加自定義注解,侵入性比較強;
因此經(jīng)過我們的商議,決定摒棄該方案,但是該方案的思想值得我們學習。接下來讓我們分析一下新的解決方案:
定義業(yè)務接口類
業(yè)務接口類包含兩個方法:具體業(yè)務處理的類型;業(yè)務的具體處理方法。
public?interface?INotifyService?{
?/**
??*?處理類型
??*/
?public?String?handleType();
?/**
??*?處理具體業(yè)務
??*/
?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 中做兩個步驟:
在 spring 啟動之后,收集所有的類型為? INotifyService的類并放入map中;將參數(shù)進行處理轉化,并驗簽處理;
private?ApplicationContext?applicationContext;
private?Map?notifyServiceMap;
/**
?*?啟動加載
?*/
@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è)務實現(xiàn)類
?INotifyService?notifyService=notifyServiceMap.get(notifyType);
?Integer?status=null;
?if(Objects.nonNull(notifyService))?{
??//執(zhí)行具體業(yè)務
??try?{
???status=notifyService.handle(JSON.toJSONString(requestParameter));
??}?catch?(Exception?e)?{
???e.printStackTrace();
??}
?}
?//后續(xù)邏輯處理
????......
????????
?return?status;
}
業(yè)務具體實現(xiàn)
@Service
public?class?NotifySignServiceImpl?implements?INotifyService?{
????@Override
????public?String?handleType()?{
????????return?"type_sign";
????}
????@Override
????@Transactional
????public?Integer?handle(String?notifyBody)?{
????????//具體的業(yè)務處理
????????......
????}
}
小結
此方案提供統(tǒng)一的異步通知入口,把公共的參數(shù)處理和驗簽邏輯與業(yè)務邏輯剝離。 利用 java 動態(tài)加載類的特性,將實現(xiàn)類通過類型進行收集。 利用 java 多態(tài)的特性,通過不同的實現(xiàn)類來處理不同的業(yè)務邏輯。
看到這,相信大家已經(jīng)對這兩種實現(xiàn)方案有了一定的理解,大家可以試著在以后的項目中應用一下,體驗一把!
END


頂級程序員:topcoding
做最好的程序員社區(qū):Java后端開發(fā)、Python、大數(shù)據(jù)、AI
一鍵三連「分享」、「點贊」和「在看」
