如何实现应用内外交互,是Quick-Fix框架的核心之一,我们常见的应用有提供web服务的(如Spring应用),有进行大数据计算的(如Storm应用),有提供rpc的后台服务(如通过dubbo提供rpc的数据服务),有纯jar服务等;基本上我们可以划分为两类

  • 应用本身,有一套健全的与外界交互的机制(这里不包括db/redis等数据的读写)
  • 应用只关注自己的服务功能(接收数据,产生数据,保存数据),本身不与第三方的应用进行交互

针对上面这两种case,我们应该怎么来设计一套应用内外交互的方案,来实现接收外部请求,执行应用内部方法或访问应用内部数据,并返回结果的目的?

I. 交互规范设计

因为不同的应用,与外部交互的方式不一样,我们希望最好能直接复用已有的通信机制来实现我们的需求;比如原来就提供了web服务,我们可以在原有的web服务的基础上,新增一个Controller来实现需求;如果应用本身是通过rpc进行通行的,且已经有了非常完善的rpc测试辅助工具,然后就希望可以直接在现在已有的rpc基础上,新增一个服务来实现应用内外交互;再如果我就是一个独立jar应用,我希望通过http方式与外界交互,所以最好框架本身就提供一种默认的交互方案

简单来讲,交互,不能写死,最好是有一个规范,具体想用哪个实现,可以交给实际使用方来选择和扩展

1. 请求参数确认

内外交互,首要的就是确认交互的参数,确定需要哪些基本信息,可以达到我们的目的;以及结果怎么返回

在设计参数之前,再次明确一下我们的目的:

  • 执行应用内某个方法
  • 访问应用中数据

要实现上面的目的,我们需要些什么?

  • service: 用于定位访问的具体类
  • field: 用来定位应用中的数据
  • method: 需要执行的方法
  • params: 方法执行需要传入参数

所以我们的req接口如下

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
@Data
@NoArgsConstructor
@AllArgsConstructor
public class FixReqDTO implements Serializable {
private static final long serialVersionUID = -151408688916877734L;
/**
* 调用的服务名,.class 结尾,则表示根据类型查找Spring容器中的Bean;否则表示传入的为beanName,通过name方式查找Spring容器中的Bean
*/
private String service;

/**
* type用来区分service传入的是bean还是静态类
*
* 当type == static 时要求service传入对应的静态类完整包路径方式
*/
private String type;

/**
* 需要执行的方法
*/
private String method;

/**
* 非空:表示最终执行的是service这个bean中成员field的方法method
* 空: 表示最终执行的是service这个bean提供的方法method
*/
private String field;

/**
* 请求参数,格式为 class#value, 如
*
* - int#20
* - Integer#20
* - String#Hello World
* - net.finbtc.component.model.TradePairDO#{"pairId": 120}
*/
private String[] params;
}

2. 请求参数说明

针对请求参数,进行解释

key 类型 解释
service String 需要执行的服务,可以是完成路径,可以是beanName
field String 需要访问的服务内部成员属性,值为属性名;为空时,表示执行的服务的某个方法
method String 方法名,需要执行的方法;为空时,表示访问某个服务的成员属性值
type String 用来辅助service来定位具体执行的类,如static表示访问的是一个静态类;single表示访问的是单例
params 数组 请求参数,数组,可以不存在,格式为 类型#值,对于基本类型,可以省略类型的前缀包

单独看表,可能不太好理解,下面结合实例进行说明,为什么要这么设计

a. 静态类和实例的访问

在第一篇的整体设计中,就提及到我们采用反射的方式来执行目标方法,因此拿到请求参数后,第一步就是获取执行的类,我们需要执行的是静态类还是实例,对于反射而言,这个差别可不小;因此我们就有了type字段,用来区分service的类型

type可以决定用哪个ServerLoader来加载传入的Service

比如在框架提供的两种service加载中,对type支持如下

value 说明 service取值
static 表示service传入的是一个静态类的包路径,我们需要访问的是静态类的成员或方法 service必须是类全路径
bean 或不传 或空字符串 表示service传入的是Spring的Bean(可以是beanName, 也可以是全路径),我们需要访问的是Spring中某个Bean的成员或方法 service可以穿beanName or 全路径

b. 方法执行

目标方法的执行,可以分为两种,一个是直接访问某个服务or静态类的方法;另外一个是访问某个服务or静态类的成员属性的方法

上面两种有什么区别呢?

  • 第一种,通过传入参数,看返回结果,更常见的是为了确认方法的执行逻辑是否有问题
  • 第二种,更多是查询或订正内存数据(应用内存数据,往往是以成员属性的方式存在)

所以field,method 这两个参数的组合,就是用来确认上面的两种场景的

case 组合方式 效果
case1 field 不传,或者field为空,method存在 相等于执行service的method方法
case2 field method都存在,且method为field对象可执行的方法 分步骤为先获取service中的field属性,然后执行field的method方法

c. 传参说明

方法执行,不可避免的就是传参了,参数我们将分为三类

  • 基本类型: int/long/char/boolean/float/double/byte
  • 特殊类型: String/Class/BigDecimal/BigInteger
  • 对象

接下来,我们约定下传参的规范

  • params 为数组
  • 基本类型,传参格式如 int#value
要求 格式 示例
params 数组 [“asd”, “123”]
基本类型 类型#value ”int#123", "long@12039123123", "boolean#true", "float#123.321"
BigDecimal BigDecimal#value BigDecimal#12.2
BigInteger BigInteger#value BigInteger#12
Class Class#类全路径 Class#com.git.hui.fix.example.jar.server.CalculateServer
String String#Value 或 Value "String#Hello", HelloWorld
对象 全路径#Json序列化对象 "com.git.hui.fix.example.jar.server.DemoDO#{\"key\":\"kkk\", \"value\":\"aaa11\"}"

接下来给出以下具体的参数解析逻辑

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
/**
* 根据传入的参数来解析为对应的do对象
* Created by @author yihui in 15:32 18/12/13.
*/
public class ArgumentParser {
private static final Object[] emptyArgs = new Object[]{};

public static Object[] parse(String[] args) {
if (args == null || args.length == 0) {
return emptyArgs;
}

Object[] result = new Object[args.length];
for (int i = 0; i < args.length; i++) {
result[i] = buildArgObj(args[i]);
}
return result;
}

private static Object buildArgObj(String arg) {
String[] typeValue = arg.split("#");
if (typeValue.length == 1) {
// 没有 #,把参数当成String
return arg;
} else if (typeValue.length == 2) {
// 标准的kv参数
return parseStrToObj(typeValue[0], typeValue[1]);
} else {
throw new IllegalInvokeArgumentException("Illegal invoke arg: " + arg);
}
}

private static Object parseStrToObj(String type, String value) {
try {
if ("int".equals(type) || "Integer".equals(type)) {
return Integer.parseInt(value);
} else if ("long".equals(type) || "Long".equals(type)) {
return Long.parseLong(value);
} else if ("float".equals(type) || "Float".equals(type)) {
return Float.parseFloat(value);
} else if ("double".equals(type) || "Double".equals(type)) {
return Double.parseDouble(value);
} else if ("byte".equals(type) || "Character".equals(type)) {
return Byte.parseByte(value);
} else if ("boolean".equals(type) || "Boolean".equals(type)) {
return Boolean.parseBoolean(value);
} else if ("BigDecimal".equals(type)) {
return new BigDecimal(value);
} else if ("BigInteger".equals(type)) {
return new BigInteger(type);
} else if ("String".equals(type)) {
return value;
} else if ("Class".equalsIgnoreCase(type)) {
return ArgumentParser.class.getClassLoader().loadClass(type);
} else {
Class clz = ArgumentParser.class.getClassLoader().loadClass(type);
return JSON.parseObject(value, clz);
}
} catch (Exception e) {
throw new IllegalInvokeArgumentException(
"Pare Argument to Object Error! type: " + type + " value: " + value, e);
}
}
}

II. 其他

0. 项目

1. 一灰灰Bloghttps://liuyueyi.github.io/hexblog

一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

2. 声明

尽信书则不如,已上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

3. 扫描关注

一灰灰blog

QrCode

知识星球

goals