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

背景

事情是這樣的,目前我正在參與 XXXX 項目的搭建,需要與第三方對接接口。在對方的接口中存在幾個異步通知,為了接口的安全性,需要對接口的參數(shù)進行驗簽處理。

為了方便大家對異步通知返回參數(shù)的處理,Z 同事提出要將該驗簽功能進行統(tǒng)一封裝,到時候大家只需要關(guān)注自己的業(yè)務(wù)邏輯即可。

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,然后調(diào)用下面的 resolveArgument 方法,
     *  如果不需要返回 false
     */
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(RsaVerify.class);
    }

    /**
     * 真正的解析方法,將請求中的參數(shù)值解析為某種對象
     * parameter 要解析的方法參數(shù)
     * mavContainer 當(dāng)前請求的 ModelAndViewContainer(為請求提供對模型的訪問)
     * webRequest 當(dā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();
        }
        
        //對參數(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<HandlerMethodArgumentResolver> 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è)務(wù)邏輯處理
        .....
        
        return "success";
    }
}

問題

問題一

看到這,細心的朋友應(yīng)該會有所疑問:既然這邊用到了自定義的注解,為什么不用切面來實現(xiàn),而是使用自定義的參數(shù)解析器呢?Very Good!這也是阿Q提出的疑問,同事說是因為 jackson 的反序列化動作優(yōu)先級遠高于切面的優(yōu)先級,所以還沒進入切面就已經(jīng)報反序列化失敗的錯誤了。

問題二

為什么在 controller 中注解 @RequestBody 不見了?

要回答這個問題,我們就得了解下HandlerMethodArgumentResolverComposite這個類了,以下簡稱Composite。 SpringMVC 在啟動時會將所有的參數(shù)解析器放到 Composite 中,Composite 是所有參數(shù)的一個集合。當(dāng)對參數(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)可以解決該問題了,但是該方案還有兩個不足之處:

  • 需要每一個回調(diào)都去創(chuàng)建自己的 controller 層,沒有一個對外的統(tǒng)一入口;
  • 需要在方法上添加自定義注解,侵入性比較強;

因此經(jīng)過我們的商議,決定摒棄該方案,但是該方案的思想值得我們學(xué)習(xí)。接下來讓我們分析一下新的解決方案:

定義業(yè)務(wù)接口類

業(yè)務(wù)接口類包含兩個方法:具體業(yè)務(wù)處理的類型;業(yè)務(wù)的具體處理方法。

public interface INotifyService {
    /**
     * 處理類型
     */
    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 中做兩個步驟:

  • 在 spring 啟動之后,收集所有的類型為 INotifyService的類并放入map中;
  • 將參數(shù)進行處理轉(zhuǎn)化,并驗簽處理;
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) {
    //參數(shù)處理+驗簽邏輯
    ......
        
    //獲取具體的業(yè)務(wù)實現(xiàn)類
    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ù)具體實現(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è)務(wù)邏輯剝離。
  • 利用 java 動態(tài)加載類的特性,將實現(xiàn)類通過類型進行收集。
  • 利用 java 多態(tài)的特性,通過不同的實現(xiàn)類來處理不同的業(yè)務(wù)邏輯。

看到這,相信大家已經(jīng)對這兩種實現(xiàn)方案有了一定地理解,大家可以試著在以后的項目中應(yīng)用一下,體驗一把!如果你有不同的意見或者更好的idea,歡迎聯(lián)系阿Q,添加阿Q可以加入技術(shù)交流群參與討論呦!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • ![Flask](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAW...
    極客學(xué)院Wiki閱讀 7,852評論 0 3
  • 不知不覺易趣客已經(jīng)在路上走了快一年了,感覺也該讓更多朋友認識知道易趣客,所以就謝了這篇簡介,已做創(chuàng)業(yè)記事。 易趣客...
    Physher閱讀 3,838評論 1 2
  • 雙胎妊娠有家族遺傳傾向,隨母系遺傳。有研究表明,如果孕婦本人是雙胎之一,她生雙胎的機率為1/58;若孕婦的父親或母...
    鄴水芙蓉hibiscus閱讀 3,936評論 0 2
  • 晴天,擁抱陽光,擁抱你。雨天,想念雨滴,想念你。 我可以喜歡你嗎可以啊 我還可以喜歡你嗎可以,可是你要知道我們不可...
    露薇霜凝閱讀 1,367評論 1 2

友情鏈接更多精彩內(nèi)容