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

背景

事情是這樣的,目前我正在參與 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這個類了,以下簡稱CompositeSpringMVC 在啓動時會將所有的參數解析器放到 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可以加入技術交流羣參與討論呦!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章