Kairbon'Blog.

为路由创建一个路由

字数统计: 2.6k阅读时长: 11 min
2022/05/02

1. 简介

路由这个名词想必大家都很了解了,以网络工程的角度出发,路由指的是数据包传递到目的端的路线,而这个路线通常不是唯一的,因此有路由表作为若干条路由信息的集合体。当数据包想要去某个目的的时候查下这个表。不过作为后端开发工程师我们对路由这个词有个更具体的感觉,就是在一个接口有多个实现的时候我们需要选择一个实现。举个例子:

1
2
3
4
5
6
7
8
9
public interface TestService {

/**
* 测试方法通过id获取名字
* @param id id
* @return 中文名字
*/
String getName(String id);
}

实现一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TestServiceImpl1 implements TestService{

private static final Map<String, String> USER_NAME = new HashMap<>(2);
static {
USER_NAME.put("1", "张三");
USER_NAME.put("2", "李四");
USER_NAME.put("3", "王五");
}

@Override
public String getName(String id) {
return USER_NAME.get(id);
}

}

实现二:

1
2
3
4
5
6
7
8
public class TestServiceImpl2 implements TestService{

@Override
public String getName(String id) {
return "haha";
}

}

这时候就需要考虑我们用哪一个实现了。也就存在了路由的概念。

1.1 创建一个简单的路由

在外面的场景中路由存在的意义就是多个实现中选择一个当前上下文想要使用的实现,这个时候就需要设置一个路由算法,把入参变成路由的结果,指向一个具体的实现。比如通过下面的map,我们就可以很轻松的得知,当入参为”xx“的时候就会走向实现2。

1
2
3
4
5
6
7
8
9
10
11
private static final Map<String, String> ROUTE_MAP= new HashMap<>(2);
static {
ROUTE_MAP.put("1", "实现1");
ROUTE_MAP.put("2", "实现1");
ROUTE_MAP.put("3", "实现1");
ROUTE_MAP.put("xx", "实现2");
}

public static String route(String id) {
return ROUTE_MAP.get(id);
}

这样我们就可以知道我们入参为多少的时候应该用哪一个实现。当然这里还有一个问题,就是路由本身的实现,他通常会以各种形式出现。
举几个例子比如:

  1. 服务注册与发现中心中,基于服务的不同版本号进行路由。
  2. Http请求通过Path路由到指定的方法入口。
  3. Spring的BeanRouter。
  4. 某些FaaS的调用方式通过一个统一的网关入口去调用。
  5. ….

因此如果我们使用任意一种框架/脚手架搭建应用时,通常会在不同的地方去配置不同的路由,最终完成这个服务程序。

但有一种场景会比较麻烦,就是当这两个属于同一种类型的路由实现,但是却要分开成为两个接口或者对象调用。举个例子,就是同一个接口的实现,使用了某种RPC自带方式实现,又使用了FaaS的方式实现,开发就得感知FaaS网关和RPC的服务注册与发现中心两种路由实现,就得配置两种不同的路由策略,使用不同的接口类去调用,即使他们本质是相同的东西。如下图:
图一
那么有没有一种办法能够屏蔽这种东西呢?

2. 为路由创建一个路由。

如下图其实就是我们期望的结果。
图二

但我们还需要让路由本身无关实现,因为在Java代码中FaaS网关和RPC的返回值通常不是一个类型,但我们期待我们使用这两种实现能够不感知结果的转换,本文提出的一个方案是使用JDK自带的动态代理去实现。

首先我们可以看下FaaS和RPC的返回有什么区别。RPC的返回一般使用了interface的返回。可以说RPC做了序列化。

1
2
TestService service = new TestServiceImpl();
service.getName("3");

但如果是FaaS的返回,就需要调用类似反射/调用的方法:

1
2
3
4
5
6
7
8
9
10
11
/**
* invoke specified version (or alias) of specified function with <code>arg</code>
*
* @param functionGroup function group
* @param functionName function name
* @param functionVersion function version or alias. If null or empty, 'latest' alias will be used.
* @param arg the argument for the function
*
*/
@Nullable
Object invoke(@NotNull String functionGroup, @NotNull String functionName, @Nullable String functionVersion, @Nullable Object arg);

可以看到这两种返回有本质的不同,但其实真实的不一致也只在返回值的序列化上。我们是可以通过序列化的方式将Object序列化为原本的返回值的。

而JDK的动态代理将帮我们解决通过一个接口类去自由调用两种不同中间件平台上的实现。

2.1 使用JDK的动态代理

抛开基本的实现原理,使用JDK的动态代理(java.lang.reflect.Proxy)主要是实现InvocationHandler,这个Handler可以为实际要调用的方法提供一个入口,并且对外暴露还是原本的接口类,符合我们的需求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @param proxy the proxy instance that the method was invoked on
*
* @param method the {@code Method} instance corresponding to
* the interface method invoked on the proxy instance. The declaring
* class of the {@code Method} object will be the interface that
* the method was declared in, which may be a superinterface of the
* proxy interface that the proxy class inherits the method through.
*
* @param args an array of objects containing the values of the
* arguments passed in the method invocation on the proxy instance,
* or {@code null} if interface method takes no arguments.
* Arguments of primitive types are wrapped in instances of the
* appropriate primitive wrapper class, such as
* {@code java.lang.Integer} or {@code java.lang.Boolean}.
*/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;

JDK的文档中也写了每个参数的意义。可以看到,我们可以通过这个方法拿到用户使用的method,和args。

2.1.1 为路由创建路由。

上面已经讲述了JDK层面对我们思路的支持,但还有一件事,就是这只是API层面的统一,我实际在哪里去路由这两种不同的实现呢?

因此我们还需要一个路由,根据参数去路由实现,我们假设这可以是一个配置,我们通过读取这个配置,确定这个方法,这个路由参数下,应该走向哪一个实现。它也许长这样:

1
2
3
4
5
6
7
{
"routeKey": "${args[0].bizType}",
"routeTable": {
"1" : "FaaS",
"2" : "HSF"
}
}

上面的Json就有着路由的几个关键属性:

  1. 路由的key,保证路由和入参的动态性,让用户可以通过简单的取值表达式来确定使用哪个参数来作为路由的入口。
  2. 路由表,根据路由表达式的取值来确定走向哪种实现。

在上述的invoke方法里,我们通过解析这个配置文件,就可以实现我们的需求。当然你可以做的更多,比如把这个配置文件也作为一个统一的服务去取。甚至可以再做一个端面。让开发更好的使用。

当然这里还有一些问题,像HSF的代理类和FaaS的代理类怎么实例化,参数返回的结果如何序列化等等。因为本文更多想展示一种思路,因此不做过多展开。感兴趣可以一起讨论。

2.1.2 用JDK的动态代理有没有问题

核心我们关注几个问题:

  1. 每次创建一个动态路由都是生成一个类,会不会导致元空间(MetaSpace)(姑且假设绝大部分使用场景都使用了JDK8)膨胀?因为我们都知道JDK的动态代理的newProxyInstance,实际是新建了一个实现那个被代理类本身的所有接口的类。
  2. 性能怎么样?实例化性能和调用性能。

2.1.2.1 会不会导致元空间(MetaSpace)膨胀?

一般不会,因为有cache,可以囊括大部分场景。可以看下JDK8的这段代码:java.lang.reflect.Proxy#getProxyClass0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Generate a proxy class. Must call the checkProxyAccess method
* to perform permission checks before calling this.
*/
private static Class<?> getProxyClass0(ClassLoader loader,
Class<?>... interfaces) {
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}

// If the proxy class defined by the given loader implementing
// the given interfaces exists, this will simply return the cached copy;
// otherwise, it will create the proxy class via the ProxyClassFactory
return proxyClassCache.get(loader, interfaces);
}

2.1.2.2 性能怎么样?

工程角度出发,加载时候是调用JNI实时编译的类,性能一般不行,适合放在应用启动时做。
可以看这段代码:java.lang.reflect.Proxy.ProxyClassFactory#apply.

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
88
89
@Override
public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {

Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
for (Class<?> intf : interfaces) {
/*
* Verify that the class loader resolves the name of this
* interface to the same Class object.
*/
Class<?> interfaceClass = null;
try {
interfaceClass = Class.forName(intf.getName(), false, loader);
} catch (ClassNotFoundException e) {
}
if (interfaceClass != intf) {
throw new IllegalArgumentException(
intf + " is not visible from class loader");
}
/*
* Verify that the Class object actually represents an
* interface.
*/
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException(
interfaceClass.getName() + " is not an interface");
}
/*
* Verify that this interface is not a duplicate.
*/
if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
throw new IllegalArgumentException(
"repeated interface: " + interfaceClass.getName());
}
}

String proxyPkg = null; // package to define proxy class in
int accessFlags = Modifier.PUBLIC | Modifier.FINAL;

/*
* Record the package of a non-public proxy interface so that the
* proxy class will be defined in the same package. Verify that
* all non-public proxy interfaces are in the same package.
*/
for (Class<?> intf : interfaces) {
int flags = intf.getModifiers();
if (!Modifier.isPublic(flags)) {
accessFlags = Modifier.FINAL;
String name = intf.getName();
int n = name.lastIndexOf('.');
String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
if (proxyPkg == null) {
proxyPkg = pkg;
} else if (!pkg.equals(proxyPkg)) {
throw new IllegalArgumentException(
"non-public interfaces from different packages");
}
}
}

if (proxyPkg == null) {
// if no non-public proxy interfaces, use com.sun.proxy package
proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
}

/*
* Choose a name for the proxy class to generate.
*/
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;

/*
* Generate the specified proxy class.
*/
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
try {
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
/*
* A ClassFormatError here means that (barring bugs in the
* proxy class generation code) there was some other
* invalid aspect of the arguments supplied to the proxy
* class creation (such as virtual machine limitations
* exceeded).
*/
throw new IllegalArgumentException(e.toString());
}
}

调用时候因为是普通的方法调用,所以只要你的实现没什么问题,一般性能也就没什么问题。

3. 总结

为路由创建路由看起来是个工程项目,其实是一个设计模式。甚至可以使用于思考本身。

CATALOG
  1. 1. 1. 简介
    1. 1.1. 1.1 创建一个简单的路由
  2. 2. 2. 为路由创建一个路由。
    1. 2.1. 2.1 使用JDK的动态代理
    2. 2.2. 2.1.1 为路由创建路由。
    3. 2.3. 2.1.2 用JDK的动态代理有没有问题
      1. 2.3.1. 2.1.2.1 会不会导致元空间(MetaSpace)膨胀?
      2. 2.3.2. 2.1.2.2 性能怎么样?
  3. 3. 3. 总结