ODL基于的MDSL,其模型用YAGN语言进行定义,编程则使用yang-toolst生成的类,而产生的数据,即存储在ODL的DataStore中的数据需要经过序列化和反序列化。而序列化反序列化代码是通过Javassist动态生成的。
BindingToNormalizedNodecodec.java
public <T extends DataObject> Entry<YangInstanceIdentifier, NormalizedNode<?, ?>> toNormalizedNode(InstanceIdentifier<T> path, T data) {
return this.codecRegistry.toNormalizedNode(path, data);
}
通过上图可以看到其是将借助YANG工具生成的模型代码,以该代码构建的实例,进行序列化成通用的NormailizedNode结构。该工具存在在Cache中
private final LoadingCache<Class<? extends DataObject>, DataObjectSerializer> serializers;
this.serializers = CacheBuilder.newBuilder().weakKeys().build(new GeneratorLoader());
项目中关于YANG文件定义各种各样,不可能将每种结构都提交定义好Serializer,所以需要根据类型动态生成对应的Serializer
AbstractStreamWriterGenerator#load
Class<? extends DataObjectSerializerImplementation> cls;
try {
cls = (Class<? extends DataObjectSerializerImplementation>) ClassLoaderUtils
.loadClass(type.getClassLoader(), serializerName);
} catch (final ClassNotFoundException e) {
cls = generateSerializer(type, serializerName);
}
生成代码中参考generateSerializer
类型为YangProtectRestoreParameter,最终返回的类为YangProtectRestoreParameter$StreamWriter(动态生成)
生成的入口为AbstractStreamWriterGenerator#generateEmitter0
private CtClass generateEmitter0(final Class<?> type, final DataObjectSerializerSource source, final String serializerName) {
final CtClass product;
/*
* getSerializerBody() has side effects, such as loading classes and codecs, it should be run in model class
* loader in order to correctly reference load child classes.
*
* Furthermore the fact that getSerializedBody() can trigger other code generation to happen, we need to take
* care of this before calling instantiatePrototype(), as that will call our customizer with the lock held,
* hence any code generation will end up being blocked on the javassist lock.
*/
final String body = ClassLoaderUtils.withClassLoader(type.getClassLoader(), new Supplier<String>() {
@Override
public String get() {
return source.getSerializerBody().toString();
}
}
);
try {
product = javassist.instantiatePrototype(DataObjectSerializerPrototype.class.getName(), serializerName, new ClassCustomizer() {
@Override
public void customizeClass(final CtClass cls) throws CannotCompileException, NotFoundException {
// Generate any static fields
for (final StaticConstantDefinition def : source.getStaticConstants()) {
final CtField field = new CtField(javassist.asCtClass(def.getType()), def.getName(), cls);
field.setModifiers(Modifier.PRIVATE + Modifier.STATIC);
cls.addField(field);
}
// Replace serialize() -- may reference static fields
final CtMethod serializeTo = cls.getDeclaredMethod(SERIALIZE_METHOD_NAME, serializeArguments);
serializeTo.setBody(body);
// The prototype is not visible, so we need to take care of that
cls.setModifiers(Modifier.setPublic(cls.getModifiers()));
}
});
} catch (final NotFoundException e) {
LOG.error("Failed to instatiate serializer {}", source, e);
throw new LinkageError("Unexpected instantation problem: serializer prototype not found", e);
}
return product;
}
上面代码使用到了Javassist
Javassist是一个动态类库,可以用来检查、”动态”修改以及创建 Java类。其功能与jdk自带的反射功能类似,但比反射功能更强大。
下面通过测试用例复现该Bug
首先了解到我们定义的Forwarding明细,注意其还在一个以小写forwarding命令的包,目录结构如下:
测试用例代码如下:
package com.zte.sunquan.demo.cache;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtField;
import org.junit.Test;
/**
* Created by sunquan on 2018/8/21.
*/
public class JavaAssistTestBug {
@Test
public void testBug() throws Exception {
//动态创建一个类
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("com.zte.sunquan.demo.User");
//创建属性
CtField field03 = CtField.make("private org.opendaylight.yang.gen.v1.cx.zx.xxx.model.cim.configuration.rev170216.forwarding.A a;", cc);
CtField field04 = CtField.make("private org.opendaylight.yang.gen.v1.cx.zx.xxx.model.cim.configuration.rev170216.Forwarding b;", cc);
cc.addField(field03);
cc.addField(field04);
//创建方法
// CtMethod method01 = CtMethod.make("public org.opendaylight.yang.gen.v1.cx.zx.xx.model.cim.configuration.rev170216.forwarding.A getName(){return name;}", cc);
// CtMethod method02 = CtMethod.make("public void setName(String name){this.name = name;}", cc);
// cc.addMethod(method01);
// cc.addMethod(method02);
//添加有参构造器
CtConstructor constructor = new CtConstructor(new CtClass[]{CtClass.intType, pool.get("java.lang.String")}, cc);
constructor.setBody("{this.b=new org.opendaylight.yang.gen.v1.cx.zx.xxx.model.cim.configuration.rev170216.forwarding();}");
cc.addConstructor(constructor);
//无参构造器
// CtConstructor cons = new CtConstructor(null, cc);
// cons.setBody("{}");
// cc.addConstructor(cons);
cc.writeFile("E:/workspace/TestCompiler/src");
}
}
代码逻辑很简单,通过Javassist进行动态创建一个类User,同时定义两个属性一个构造函数,构造函数中初始化b,但注意的是其使用的是小写的forwarding,实际上是不存在小写的forwarding类的。
代码执行抛出异常:
java.lang.RuntimeException: cannot find org.opendaylight.yang.gen.v1.cx.zx.xxx.model.cim.configuration.rev170216.forwarding: org.opendaylight.yang.gen.v1.cx.zx.xxx.model.cim.configuration.rev170216.Forwarding found in org/opendaylight/yang/gen/v1/cx/zx/xxx/model/cim/configuration/rev170216/forwarding.class
异常信息表示无法找到forwarding.class这个类
仔细观察调用栈,可以发现:
真实的异常抛出是由于cf.getName()与qualifiedName不一致导致的
为何qualifiedName会是小写的forwarding,从代码逻辑判断出,如果执行到这,实际上forwarding.class是已经能够在classPool中找到的。验证:
代码走到了return new CtClassType,所以find(classname)返回并不为空,但classname实际上就是:
org.opendaylight.yang.gen.v1.cx.zx.xxx.model.cim.configuration.rev170216.forwarding
那即从AppClassLoader中居然找到了forwarding.class这个类?这明明是一个包,不可能是类,太诡异了!
再次查看目录,classPath中明明只有Forwarding.class这个类
是时候揭露真相了,即在window系统中,AppClassLoader在find类时并不区别大小写,所以其找到的实际上就是Forwarding.class,如果此时其就使用获取到的句柄执行操作,那其实没有任何问题。
但Javassist的会进一步判断类名是否一致,你传进去的是forwarding,而根据JAVA开发习惯,类名只能以大写字母开关,所以Javassist会进行一步转换将首字母大写(ClassFile构造函数中)
thisclassname = constPool.getClassInfo(thisClass);
所以出现了不一致,导致异常抛出。
修改:
1、不要命名类名与包名一致
2、Assisti源码修改,判断究竟是类还是路径