前言

近段时间在应用easyopen时,发现定义的申请体与理论参数不符时会呈现参数无奈失常传递的景象,于是就把easyopen的源码 clone 下来钻研了一波。

easyopen 测试版本

1.16.6.1

场景复现

  • 申请体定义如下
public class GoodsParam {    @ApiDocField(description = "商品名称", required = true, example = "iphoneX", name = "goods")    private String goodsName;    public String getGoodsName() {        return goodsName;    }    public void setGoodsName(String goodsName) {        this.goodsName = goodsName;    }}
  • 接口定义如下
@ApiService@ApiDoc("商品模块")public class GoodsApi {    private static final Logger log = LoggerFactory.getLogger(GoodsApi.class);    @Api(name = "goods.get")    @ApiDocMethod(description = "获取商品")    public String getGoods(GoodsParam param) {        log.info("incoming param: {}", JSONObject.toJSONString(param));        return param.getGoodsName();    }}

上述代码应用官网示例批改。

因为我是要复现出错的场景,所以我对申请体属性 goodsName 进行批改,并批改原有接口逻辑,使得接口打印申请体数据并间接返回申请体 goodsName 属性。

启动我的项目,申请接口 goods.get,日志与后果截图如下

接口并未如文档定义个别拿到申请体的参数,场景复现。

寻找起因

代码的益处是,0 即是 0,1 就是 1,如果 0 变成 1,那就是你写的 bug 或者你无意为之,这个场景必然有代码作为撑持。

因为研读源码的教训较浅,所以一开始从框架打印日志动手。

程序在启动时总会打这几行日志,所以就全局搜寻了一波,定位到 src\main\java\com\gitee\easyopen\register\ApiRegister.java 的 doWith 办法中。

@Overridepublic void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {    checkTransactionalAnnotation(method);     ReflectionUtils.makeAccessible(method);     Api api = AnnotationUtils.findAnnotation(method, Api.class);     boolean ignoreSign = api.ignoreSign() ? true : this.apiServiceAnno.ignoreSign();     boolean ignoreValidate = api.ignoreValidate() ? true : this.apiServiceAnno.ignoreValidate();     boolean isWrapResult = this.apiServiceAnno.wrapResult() ? api.wrapResult() : false;     ApiDefinition apiDefinition = new ApiDefinition();     //apiDefinition setter....     Parameter[] parameters = method.getParameters();     Class<?> paramClass = null;     if (parameters != null && parameters.length > 0) {         Parameter parameter = parameters[0];        paramClass = parameter.getType();        boolean isNumberOrStringType = FieldUtil.isNumberOrStringType(paramClass);        apiDefinition.setSingleParameter(isNumberOrStringType);        apiDefinition.setMethodArguClass(paramClass);        if (isNumberOrStringType) {            SingleParameterContext.add(handler, method, parameter, api);        }     }     logger.debug("注册接口name={},version={},method={} {}({})", api.name(), api.version(),     method.getReturnType().getName(), method.getName(), paramClass == null ? "" : paramClass.getName());      try {        DefinitionHolder.addApiDefinition(apiDefinition);        apiConfig.getApiRegistEvent().onSuccess(apiDefinition);     } catch (DuplicateApiNameException e) {        logger.error(e.getMessage(), e);        System.exit(0);     }     apiCount++;}public static void addApiDefinition(ApiDefinition apiDefinition) throws DuplicateApiNameException {    String key = getKey(apiDefinition);    boolean hasApi = apiDefinitionMap.containsKey(key);    if (hasApi) {        throw new DuplicateApiNameException("反复申明接口,name:" + apiDefinition.getName() + " ,version:"+ apiDefinition.getVersion() + ",method:" + apiDefinition.getMethod().getName());    }    apiDefinitionMap.put(key, apiDefinition);}

浏览代码发现,这一段代码只负责定义接口的解析并造成一个 String to ApiDefinition 的 Map,并没有申请体参数转换逻辑,所以这不是目的地。

然而这么大段代码只造成一个 Map,这个 Map 必然有其作用。于是寻找调用这个 Map 的 getter 办法,最初定位到 src\main\java\com\gitee\easyopen\register\ApiInvoker.java 的 doInvoke 办法中。

protected Object doInvoke(ApiParam param, HttpServletRequest request, HttpServletResponse response) throws Throwable {    ApiDefinition apiDefinition = this.getApiDefinition(param);    ApiContext.setApiMeta(apiDefinition);    if (!apiDefinition.isIgnoreJWT()) {        this.initJwtInfo(request, param);    }    // 办法参数    Object methodArgu = null;    // 返回后果    Object invokeResult = null;    Validator validator = ApiContext.getApiConfig().getValidator();    param.setIgnoreSign(apiDefinition.isIgnoreSign());    param.setIgnoreValidate(apiDefinition.isIgnoreValidate());    // 验证操作,这里有负责验证签名参数    validator.validate(param);    // 业务参数json格局    String busiJsonData = ApiContext.getApiConfig().getDataDecoder().decode(param);    // 业务参数Class    Class<?> arguClass = apiDefinition.getMethodArguClass();    boolean isSingleParameter = apiDefinition.isSingleParameter();    Object singleParamProxy = null;    int interceptorIndex = 0;    try {        // 将参数绑定到业务办法参数上,业务办法参数能够定义的类型:JSONObject,Map<String,Object>,String,业务参数类        if (arguClass != null) {            if(arguClass == JSONObject.class) {                methodArgu = JSON.parseObject(busiJsonData);            } else if(arguClass == Map.class) {                methodArgu = new HashMap<String,Object>(JSON.parseObject(busiJsonData));            } else if(isSingleParameter) {                SingleParameterContext.SingleParameterContextValue value = SingleParameterContext.get(param.fatchName(), param.fatchVersion());                if (value != null) {                    JSONObject jsonObj = JSON.parseObject(busiJsonData);                    methodArgu = jsonObj.getObject(value.getParamName(), arguClass);                    singleParamProxy = jsonObj.toJavaObject(value.getWrapClass());                }            } else {                methodArgu = JSON.parseObject(busiJsonData, arguClass);            }            this.bindUploadFile(methodArgu);        }        // 拦截器        ApiInterceptor[] interceptors = ApiContext.getApiConfig().getInterceptors();        if(interceptors == null) {            interceptors = EMPTY_INTERCEPTOR_ARRAY;        }        //1. 调用preHandle        for (int i = 0; i < interceptors.length; i++) {            ApiInterceptor interceptor = interceptors[i];              if (interceptor.match(apiDefinition) && !interceptor.preHandle(request, response, apiDefinition.getHandler(),methodArgu)) {                //1.1、失败时触发afterCompletion的调用                  triggerAfterCompletion(apiDefinition, interceptorIndex, request, response, methodArgu, null,null);                return null;              }            //1.2、记录以后预处理胜利的索引            interceptorIndex = i;        }        // 验证业务参数JSR-303        this.validateBizArgu(validator, methodArgu, singleParamProxy);        /* *** 调用业务办法,被@Api标记的办法 ***/        MethodCaller methodCaller = apiDefinition.getMethodCaller();        if (methodCaller != null) {            invokeResult = methodCaller.call(new ApiInvocation(apiDefinition, methodArgu));        } else {            invokeResult = Callers.call(apiDefinition, methodArgu);        }        //3、调用postHandle,业务办法调用后处理(逆序)          for (int i = interceptors.length - 1; i >= 0; i--) {              ApiInterceptor interceptor = interceptors[i];              if(interceptor.match(apiDefinition)) {                interceptor.postHandle(request, response, apiDefinition.getHandler(), methodArgu, invokeResult);            }        }          if(invokeResult == null) {            invokeResult = EMPTY_OBJECT;        }        // 最终返回的对象        Object finalReturn = this.wrapResult(apiDefinition, invokeResult);        setMsg(finalReturn);        //4、触发整个申请处理完毕回调办法afterCompletion          triggerAfterCompletion(apiDefinition, interceptorIndex, request, response, methodArgu, finalReturn, null);        return finalReturn;    } catch (Exception e) {        this.triggerAfterCompletion(apiDefinition, interceptorIndex, request, response, methodArgu, invokeResult, e);        throw e;    }}
 浏览代码发现,此段代码次要做了四个事件:解析网络申请数据,从  apiDefinitionMap 获取接口对应配置,反射调用对应接口,包装调用接口返回后果并返回。参数解析必然在此段代码中解决!

通过断点 debug 查看数据得悉,参数解析集中于以下代码

if (arguClass != null) {    if (arguClass == JSONObject.class) {        methodArgu = JSON.parseObject(busiJsonData);    } else if (arguClass == Map.class) {        methodArgu = new HashMap(JSON.parseObject(busiJsonData));    } else if (isSingleParameter) {        SingleParameterContextValue value = SingleParameterContext.get(apiDefinition.getMethod());        if (value != null) {            JSONObject jsonObj = JSON.parseObject(busiJsonData);            methodArgu = jsonObj.getObject(value.getParamName(), arguClass);            singleParamProxy = jsonObj.toJavaObject(value.getWrapClass());        }    } else {        methodArgu = JSON.parseObject(busiJsonData, arguClass);    }    this.bindUploadFile(methodArgu);}

step into断点得悉,easyopen 依赖 fastjson 的序列化机制进行参数的序列化,起因得悉:easyopen 应用 fastjson 序列化参数成为参数类获取参数,所以当 @ApiDocField 的属性 name 与参数类对应的属性名不统一时,属性值获取失败。

    本文首发于 cartoon的博客
转载请注明出处:https://cartoonyu.github.io/cartoon-blog/post/source-code-unscramble/easyopen参数无奈失常传递景象解析