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