0%

easyopen 参数无法正常传递现象解析

前言

近段时间在使用easyopen时,发现定义的请求体与实际参数不符时会出现参数无法正常传递的现象,于是就把easyopen的源码 clone 下来研究了一波。

easyopen 测试版本

1
1.16.6.1

场景复现

  • 请求体定义如下
1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}
}
  • 接口定义如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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();
}

}
上述代码使用[官方示例](https://gitee.com/durcframework/easyopen/tree/master/easyopen-demo/easyopen-server-normal)修改。

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

启动项目,请求接口 [goods.get](http://localhost:8081/api/doc#201),日志与结果截图如下

接口并未如文档定义一般拿到请求体的参数,场景复现。

寻找原因

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

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

 程序在启动时总会打这几行日志,所以就全局搜索了一波,定位到 src\main\java\com\gitee\easyopen\register\ApiRegister.java 的 doWith 方法中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Override
public 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 方法中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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 查看数据得知,参数解析集中于以下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 与参数类对应的属性名不一致时,属性值获取失败。

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