背景
由于开发过程中,个别dubbo接口的调用会在服务发布过程中,出现P99耗时报警问题。因此我们计划通过预热服务接口,通过预热来触发JIT,构建DB资源长链接。实现服务接口上线后,耗时过长,资源等待等问题!
预热实现原理
对于预热的方案,这里做简单阐述,我们定义了一个预热协议,该协议可动态加减预热接口,协议简述如下:
// contract model
{
"warmupList": [
{
"bean": "exampleAServeice",
"method": "methodA",
"case": [
"{\"name\":\"mongo\"}",
"{\"name\":\"h-mongo\"}"
], // 预热case列表,object参数这里填写转义的json,注意这个后面会提及到
"type": "object", //
"invokes": 3 // 执行次数
},
{
"bean": "exampleBServeice",
"method": "methodB",
"case": [
"[12345]",
"[12345,678910]"
], // 预热case列表,object参数这里填写转义的json,注意这个后面会提及到
"type": "object", //
"invokes": 3 // 执行次数
}
],
"timeout": 200 // 所有预热执行完成的超时时间
}
解析预热模型中的bean节点,然后通过SpringApplicationContext#getBean(beanName)
方法来获取bean实例,反射找到对应的method来执行case!实现的原理也很简单粗暴(如果有更优秀的预热方案,欢迎评论区指教)!
对于类型的method参数类型通过如下的代码来确定:
// 这里仅考虑单参数
Type type = method.getGenericParameterTypes()[0]
发现问题
使用该协议对几个业务系统的接口预热都没有出现问题(大概是运气好),直到遇见了系统X,居然报出来了问题!!!这次我们选择预热的接口形式如下:
@Data
public class Request<T> implements Serializable{
private int channel;
private T param;
}
@Data
public class AChannelParam implements Serializable{
private Stinrg p1;
private Stinrg p2;
}
interface ExampleService {
String methodX(Request<AChannelParam > request);
}
public class ExampleServiceImpl implements ExampleService {
@Override
public String methodX(Request<AChannelParam> request) {
// 业务逻辑处理
return "success";
}
}
这个接口方法的入参是一个泛型化的参数,case采用json形式的具体值,通过jackson的ObjecMapper的TypeReference来进行包装解析即可,如下示例:
Type type = execMethod.getGenericParameterTypes()[0]
TypeReference trf = new TypeReference<T>() {
@Override
public Type getType() {
return type;
}
};
Request<AChannelParam> req = objectMapper.readValue("{\"channel\":1,\"param\":{\"p1\":\"v1\",\"p2\":\"v2\"}}", trf);
然而结果却大相径庭,解析失败了,req实例的param属性为null,json反序列化的过程中,数据丢失了!!!
定位问题
当下对该case进行了debug,通过debug的数据信息发现我们通过SpringApplicationContext拿到的bean是一个CGLIB代理类,其实了解动态代理的应该都知道代理类会生成一个与被代理类目标一样的method,但是spring的这个代理机制丢失了原始method的signature信息,因此我们也没办法通过反射method获取到参数类型的原始类型数据。普通的bean不会出现这个问题,派生类和接口实现类则回出现代理的问题,当然这里没有考虑AOP的影响!
由于debug的信息并未截图保存,这里没法展示。其实也很容易证实,构建一个简单spring-demo,定义一个服务接口,以及一个接口实现类,一个controller并注入这个服务接口。在debug模式下,调用
bean.getClass().getCanonicalName()
即可发现类名带有“$CGLIB”
字符串,这里的bean代指注入controller中的那个!
解决问题
既然当前获取的是代理类中的同名方法,那么我们只要能拿到被代理类的方法自然就能获取原方法参数的类型定义!既然是代理类,那么自然可以获取到被代理的类定义即Class对象,代理对象的方法部分列表如图所示:
这里注意圈出来的那个方法,通过方法名即可看出来,这个就是获取被代理类的Class对象方法,因此我们只需要通过反射调用这个方法即可完成获取到原method实例,通过这个method来获取参数类型,通过修复代码测试验证后,泛型方法的预热问题也就迎刃而解了!!!
今日格言:对源码及原理深入,将会使我们对突发问题的态度由慌张而专向冷静!