看了同事寫(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<HandlerMethodArgumentResolver> 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<String,INotifyService> notifyServiceMap;
/**
* 啟動(dòng)加載
*/
@PostConstruct
public void init(){
Map<String,INotifyService> map = applicationContext.getBeansOfType(INotifyService.class);
Collection<INotifyService> services = map.values();
if(CollectionUtils.isEmpty(services)){
return;
}
notifyServiceMap = services.stream().collect(Collectors.toMap(INotifyService::handleType, x -> x));
}
@Override
public Map<String, INotifyService> 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)一把!
程序汪資料鏈接
程序汪接的7個(gè)私活都在這里,經(jīng)驗(yàn)整理
Java項(xiàng)目分享 最新整理全集,找項(xiàng)目不累啦 07版
堪稱(chēng)神級(jí)的Spring Boot手冊(cè),從基礎(chǔ)入門(mén)到實(shí)戰(zhàn)進(jìn)階
臥槽!字節(jié)跳動(dòng)《算法中文手冊(cè)》火了,完整版 PDF 開(kāi)放下載!
臥槽!阿里大佬總結(jié)的《圖解Java》火了,完整版PDF開(kāi)放下載!
字節(jié)跳動(dòng)總結(jié)的設(shè)計(jì)模式 PDF 火了,完整版開(kāi)放下載!
歡迎添加程序汪個(gè)人微信 itwang009 進(jìn)粉絲群或圍觀朋友圈
