动态代理调用的实际运用

动态代理在实际工作中很难用到,通常都是一些底层组件才会使用,比如SpringAOP。但由于业务正在做“降本增效”的多租户改造,因此正好有了使用机会。

我们的业务分布在6个国家,每个国家都有独立的服务器、数据库、中间件,然而每个国家的用户数、使用APP时间段、使用习惯等各种因素导致服务器资源的使用效率不高:有的服务可能CPU在1%~10%使用率,为了高可用却仍然需要至少4台服务器,资源会有浪费。现在要做的就是把业务、服务器、数据库均合并,通过全链路携带“租户标记TenantId”来区分请求来源,所有国家共用服务器和数据库资源。

实现的方式很简单,类似skywalking这种tracing组件,利用ThreadLocal等数据结构,将类似“业务_国家_语言_货币单位”这种请求标记全链路传递。

multi-tanant.png

由于时间非常紧张,集团大佬写的多租户中间件,调用上比较繁琐,每次RPC调用前,都得设置一次TenantId,因为同个线程可能会承载各个国家各个语言的用户,并不是固定的。这给业务带来一些不必要的代码,看着会很烦。于是我们组的大佬写出了一个代理类,在创建RPC consumer bean时直接代理,隐藏这些设置多租户的业务无关的代码。里面有一些精妙的思路,非常值得分析。

1、创建代理

1
2
3
4
5
@SuppressWarnings("unchecked")
public static <T> T createHsfConsumer(Class<T> hsfInterface, String version, String... targetUnits) {
InvocationHandler h = new LandlordHsfInvocationHandler(hsfInterface, version, targetUnits);
return (T) Proxy.newProxyInstance(hsfInterface.getClassLoader(), new Class[]{hsfInterface}, h);
}

创建出来的bean会被spring容器管理,忽略使用细节,这里重点分析动态代理。

动态代理分JDK动态代理和CGLIB动态代理。JDK动态代理基于接口创建代理对象,利用反射机制生成包含被代理对象所有接口的代理类,覆盖接口中的所有方法。而CGLIB代理则是基于继承的方式生成子类,添加代理逻辑,不受限于接口。这里是RPC调用,很适合JDK动态代理。这里的细节是,使用的是hsfInterface的classloader,因为hsf是被pandora管理的(pandora是一个利用不同类加载器来加载完全相同的两个类,从而避免依赖的各个中间件有冲突的组件)。

2、handler

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
private static class LandlordHsfInvocationHandler implements InvocationHandler {
private final Class<?> hsfInterface;
private final String version;
private final List<String> targetUnits;
private final Object hsfObject;

LandlordHsfInvocationHandler(Class<?> hsfInterface, String version, String... targetUnits) {
this.hsfInterface = hsfInterface;
this.version = version;
this.targetUnits = Arrays.asList(targetUnits);
try {
HSFApiConsumerBean consumer = new HSFApiConsumerBean();
consumer.setInterfaceClass(hsfInterface);
consumer.setIncludeFilters(Collections.singletonList("cluster-filter"));
consumer.setIncludeRouters(Collections.singletonList("cluster-router"));
consumer.setConfigserverCenter(this.targetUnits);
consumer.setGroup("HSF");
consumer.setVersion(version);
consumer.init();
this.hsfObject = consumer.getObject();
} catch (Exception e) {
throw new RuntimeException(e);
}
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
switch (method.getName()) {
case "equals":
return proxy == args[0];
case "hashCode":
return System.identityHashCode(proxy);
case "toString":
return "Cross Unit HSF proxy, interface: " + hsfInterface.getName() + ", version: " + version;
default:
RequestCtxUtil.setTargetCluster(targetUnits.get(RandomUtils.nextInt(0, targetUnits.size())));
LandlordHSFUtils.setTenantIdForNextInvoke(LandlordContext.getCurrentTenantId());
return method.invoke(hsfObject, args);
}
}
}

代码很容易理解,反射调用invoke之前,做一些“配置多租户、选择服务器可用区”等业务无关的操作。

亮点在invoke重写了equals、hashcode、toString方法的代理,这一段其实抄的是spring源码ObjectFactoryDelegatingInvocationHandler类,里面有对应的解释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {  
String methodName = method.getName();
if (methodName.equals("equals")) {
// Only consider equal when proxies are identical.
return (proxy == args[0]);
}
else if (methodName.equals("hashCode")) {
// Use hashCode of proxy.
return System.identityHashCode(proxy);
}
else if (methodName.equals("toString")) {
return this.objectFactory.toString();
}
try {
return method.invoke(this.objectFactory.getObject(), args);
}
catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}

这也是代理类比较通用和经典的写法。

动态代理调用的实际运用

https://www.bananaoven.com/posts/43070/

作者

香蕉微波炉

发布于

2024-08-09

更新于

2024-08-09

许可协议