概述
在Dubbo自适应扩展中,我们已经得到了自适应扩展类的字符串,需要通过编译才能得到真正的Class,本篇文章就来介绍将类的字符串编译成类的过程。
动态编译
dubbo 的动态编译的整体结构如上图所示。dubbo中的Compiler基于dubbo spi机制进行加载,目前支持jdk和javassist两种实现:
<dubbo:application compiler="jdk" />
<dubbo:application compiler="javassist" />
整体了解了dubbo的动态编译后,我们接着上一篇文章继续分析,dubbo动态编译入口的代码如下:
private Class<?> createAdaptiveExtensionClass() {
// 生成自适应拓展实现的代码字符串
String code = createAdaptiveExtensionClassCode();
// 获取类加载器
ClassLoader classLoader = findClassLoader();
// 获取Compiler自适应扩展对象
com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
// 动态编译,生成Class
return compiler.compile(code, classLoader);
}
该方法在编译阶段需要先获取自适应编译对象,然后调用该对象的compile方法进行代码的编译。其实这里并不是直接使用自适应对象进行代码编译,而是将具体的编译任务交给子类来完成,即JdkCompiler子类和JavassistCompiler子类,下面我们来看看dubbo 动态编译的成员及它们的用途。
Compiler 扩展接口
/**
* Compiler. (SPI, Singleton, ThreadSafe)
* 使用Dubbo SPI机制,默认拓展名为javassist
*/
@SPI("javassist")
public interface Compiler {
/**
* 编译Java 代码
*
* @param code Java代码字符串
* @param classLoader 类加载器
* @return Compiled class 编译后的类
*/
Class<?> compile(String code, ClassLoader classLoader);
}
AdaptiveCompiler 固定自适应扩展类
/**
* AdaptiveCompiler. (SPI, Singleton, ThreadSafe)
* 实现Compiler接口,带有@Adaptive注解,是固定的自适应实现类
*/
@Adaptive
public class AdaptiveCompiler implements Compiler {
/**
* 默认编辑器的拓展名
*/
private static volatile String DEFAULT_COMPILER;
/**
* 静态方法,设置默认编辑器的拓展名。该方法被 {@link com.alibaba.dubbo.config.ApplicationConfig#setCompiler(java.lang.String)}方法调用.
* 在<dubbo:application compiler=""/> 配置 可触发该方法
*
* @param compiler
*/
public static void setDefaultCompiler(String compiler) {
DEFAULT_COMPILER = compiler;
}
@Override
public Class<?> compile(String code, ClassLoader classLoader) {
Compiler compiler;
// 获得Compiler的ExtensionLoader对象
ExtensionLoader<Compiler> loader = ExtensionLoader.getExtensionLoader(Compiler.class);
// 声明 name 变量
String name = DEFAULT_COMPILER;
// 使用设置的拓展名,获得Compiler拓展对象
if (name != null && name.length() > 0) {
compiler = loader.getExtension(name);
// 获得默认的Compiler拓展对象
} else {
compiler = loader.getDefaultExtension();
}
// 使用真正的Compiler对象,动态编译代码
return compiler.compile(code, classLoader);
}
}
该类使用了@Adaptive注解,说明AdaptiveCompiler会固定为默认实现,通过代码的逻辑不难发现,该类主要用来管理其它的Compiler,每次调用compiler方法时会尝试根据扩展名获取Compiler的扩展对象,默认情况下使用JavassistCompiler扩展对象,然后使用编译对象进行动态编译代码串。
AbstractCompiler 抽象编译类
public abstract class AbstractCompiler implements Compiler {
/**
* 包名的正则表达式,注意匹配组
*/
private static final Pattern PACKAGE_PATTERN = Pattern.compile("package\\s+([$_a-zA-Z][$_a-zA-Z0-9\\.]*);");
/**
* 类名的正则表达式,注意匹配组
*/
private static final Pattern CLASS_PATTERN = Pattern.compile("class\\s+([$_a-zA-Z][$_a-zA-Z0-9]*)\\s+");
@Override
public Class<?> compile(String code, ClassLoader classLoader) {
// 获得包名
code = code.trim();
Matcher matcher = PACKAGE_PATTERN.matcher(code);
String pkg;
if (matcher.find()) {
pkg = matcher.group(1);
} else {
pkg = "";
}
// 获得类名
matcher = CLASS_PATTERN.matcher(code);
String cls;
if (matcher.find()) {
cls = matcher.group(1);
} else {
throw new IllegalArgumentException("No such class name in " + code);
}
// 获得完整类名: 包名.类名
String className = pkg != null && pkg.length() > 0 ? pkg + "." + cls : cls;
try {
// 使用类加载器尝试加载类,如果加载成功,说明已经存在(可能编译过了)
return Class.forName(className, true, ClassHelper.getCallerClassLoader(getClass()));
// 如果加载失败,可能类不存在,说明可能未编译过,就进行编译
} catch (ClassNotFoundException e) {
// 代码格式验证
if (!code.endsWith("}")) {
throw new IllegalStateException("The java code not endsWith \"}\", code: \n" + code + "\n");
}
try {
// 使用具体的编译器进行代码编译,由子类实现
return doCompile(className, code);
} catch (RuntimeException t) {
throw t;
} catch (Throwable t) {
throw new IllegalStateException("Failed to compile class, cause: " + t.getMessage() + ", class: " + className + ", code: \n" + code + "\n, stack: " + ClassUtils.toString(t));
}
}
}
/**
* 编译代码
*
* @param name 类名
* @param source 代码串
* @return 编译后的类
* @throws Throwable 异常
*/
protected abstract Class<?> doCompile(String name, String source) throws Throwable;
}
该抽象类主要做两件事情,先获取要编译的字符串中的类的全路径名,根据类名尝试加载对应的类,如果加载成功说明已经编译过了,就直接返回即可,防止重复编译。如果加载失败,那么就需要进行编译处理。接下来将编译的任务交给具体的子类来完成。
JavassistCompiler 编译器
在介绍JavassistCompiler编译器前,我们需要先简单了解下Javassist,这样就能很好理解JavassistCompiler的逻辑了。Javassist是用来处理java字节码的类库,可以进行分析、编辑和创建Java字节码,它提供了丰富的API,可以使开发人员很方便操作字节码。不仅如此,我们知道处理Java字节码的工具很多,如cglib,asm等,为什么选择Javassist呢?因为Javassist简单且快速,可以直接使用Java编码的方式而不需要了解虚拟机指令就能动态改变类的结构,或者动态生成类。下面我们来看下javassist的几个API,dubbo就是使用javassist的API来动态生成类的。
- 读取Class
// 获取默认的ClassPool(搜索类路径只是JVM的同路径下的class),是一个Javassist的类池
ClassPool pool = ClassPool.getDefault();
//从classpath中查询类Xxx
CtClass cc = pool.get("Xxx");
//设置Xxx的父类Yyy
cc.setSuperclass(pool.get("Yyy"));
// 转为字节数组,进行CtClass的冻结
byte[] b=cc.toBytecode();
// 生成class 类,默认加载到当前线程的ClassLoader中,也可以选择输出的ClassLoader。
Class clazz=cc.toClass();
// 修改读取的Class的name,这样会创建一个新的Class,旧的不会删除
cc.setName("XxxTemp");
// 其它api
...
- 创建Class
ClassPool pool = ClassPool.getDefault();
// 创建一个Xxx类
CtClass cc = pool.makeClass("Xxx");
//新增方法
CtMethod m = CtNewMethod.make("public void test(){System.out.print(hello world)}",cc);
cc.addMethod(m);
//新增Field
CtField f = new CtField(CtClass.intType, "a", point);
cc.addField(f);
//引入包
pool.importPackage("package");
// 其它api
...
- 搜索路径
ClassPool pool = ClassPool.getDefault();
//默认加载方式如
pool.insertClassPath(new ClassClassPath(this.getClass()));
//从文件加载classpath
pool.insertClassPath("filepath")
//从URL中加载
pool.insertClassPath(new URLClassPath("xxx"));
//追加 LoaderClassPath
pool.appendClassPath(new LoaderClassPath(ClassHelper.getCallerClassLoader(getClass())));
- 具体操作示例
public class JavassistCompilerDemo {
public static void main(String[] args) throws Exception {
// 创建
createStudentClass();
// 读取
readStudentClass();
}
/**
* 创建字节码信息
*
* @throws Exception
*/
private static void createStudentClass() throws Exception {
// 创建ClassPool
ClassPool pool = ClassPool.getDefault();
// 创建 com.alibaba.dubbo.test.Student 类
CtClass ctClass = pool.makeClass("com.alibaba.dubbo.test.Student");
// 创建属性(通用形式)
CtField nameField = CtField.make("private String name;", ctClass);
ctClass.addField(nameField);
// API形式创建属性
CtField ageField = new CtField(pool.getCtClass("int"), "age", ctClass);
ageField.setModifiers(Modifier.PRIVATE);
ctClass.addField(ageField);
// 创建方法 (通用方式)
CtMethod setName = CtMethod.make("public void setName(String name){this.name = name;}", ctClass);
CtMethod getName = CtMethod.make("public String getName(){return name;}", ctClass);
ctClass.addMethod(setName);
ctClass.addMethod(getName);
// api形式创建方法
ctClass.addMethod(CtNewMethod.getter("getAge", ageField));
ctClass.addMethod(CtNewMethod.setter("setAge", ageField));
//创建无参构造方法
CtConstructor ctConstructor = new CtConstructor(null, ctClass);
ctConstructor.setBody("{}");
ctClass.addConstructor(ctConstructor);
// 创建有参构造方法
CtConstructor constructor = new CtConstructor(new CtClass[]{CtClass.intType, pool.get("java.lang.String")}, ctClass);
constructor.setBody("{this.age=age;this.name=name;}");
ctClass.addConstructor(constructor);
// api创建普通方法
CtMethod ctMethod = new CtMethod(CtClass.voidType, "sayHello", new CtClass[]{}, ctClass);
ctMethod.setModifiers(Modifier.PUBLIC);
ctMethod.setBody(new StringBuilder("{\n System.out.println(\"hello world!\"); \n}").toString());
ctClass.addMethod(ctMethod);
// 生成class 类
Class<?> clazz = ctClass.toClass();
// 反射创建对象
Object obj = clazz.newInstance();
//方法调用
obj.getClass().getMethod("sayHello", new Class[]{}).invoke(obj);
// 获取ctClass的字节码
byte[] codeByteArray = ctClass.toBytecode();
// 将字节码写入到class文件中
FileOutputStream fos = new FileOutputStream(new File("/opt/test/Student.class"));
fos.write(codeByteArray);
fos.close();
}
/**
* 访问已存在的字节码信息
*
* @throws Exception
*/
private static void readStudentClass() throws Exception {
// 创建ClassPool
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.get("com.alibaba.dubbo.test.Student");
//得到字节码
byte[] bytes = ctClass.toBytecode();
System.out.println(Arrays.toString(bytes));
//获取类名
System.out.println(ctClass.getName());
//获取接口
System.out.println(Arrays.toString(ctClass.getInterfaces()));
//获取方法列表
System.out.println(Arrays.toString(ctClass.getMethods()));
}
}
运行上面的代码会在本地/opt/test文件目录下生成了一个Student.class文件,我们通过 javap
命令进行反编译的结果如下:
$ javap Student.class
Compiled from "Student.java"
public class com.alibaba.dubbo.test.Student {
public void setName(java.lang.String);
public java.lang.String getName();
public int getAge();
public void setAge(int);
public com.alibaba.dubbo.test.Student();
public com.alibaba.dubbo.test.Student(int, java.lang.String);
public void sayHello();
}
可以清楚地看到,通过Javassist把一个完整的class字符串编译成为一个Class,有了这个案例的铺垫我们就很容易理解JavassistCompiler的原理了,让我们一起来看看它的逻辑。
/**
* JavassistCompiler. (SPI, Singleton, ThreadSafe)
* 基于 Javassist 实现的 Compiler
*/
public class JavassistCompiler extends AbstractCompiler {
/**
* 匹配import
*/
private static final Pattern IMPORT_PATTERN = Pattern.compile("import\\s+([\\w\\.\\*]+);\n");
/**
* 匹配 extents
*/
private static final Pattern EXTENDS_PATTERN = Pattern.compile("\\s+extends\\s+([\\w\\.]+)[^\\{]*\\{\n");
/**
* 匹配 implements
*/
private static final Pattern IMPLEMENTS_PATTERN = Pattern.compile("\\s+implements\\s+([\\w\\.]+)\\s*\\{\n");
/**
* 正则匹配方法/属性
*/
private static final Pattern METHODS_PATTERN = Pattern.compile("\n(private|public|protected)\\s+");
/**
* 正则匹配变量
*/
private static final Pattern FIELD_PATTERN = Pattern.compile("[^\n]+=[^\n]+;");
@Override
public Class<?> doCompile(String name, String source) throws Throwable {
// 获得类名
int i = name.lastIndexOf('.');
String className = i < 0 ? name : name.substring(i + 1);
// 创建ClassPoll对象
ClassPool pool = new ClassPool(true);
// 设置类搜索路径
pool.appendClassPath(new LoaderClassPath(ClassHelper.getCallerClassLoader(getClass())));
// 匹配import
Matcher matcher = IMPORT_PATTERN.matcher(source);
// 引入包名
List<String> importPackages = new ArrayList<String>();
// 引入类名
Map<String, String> fullNames = new HashMap<String, String>();
// 匹配import,导入依赖包
while (matcher.find()) {
String pkg = matcher.group(1);
// 导入整个包下的类/接口
if (pkg.endsWith(".*")) {
String pkgName = pkg.substring(0, pkg.length() - 2);
pool.importPackage(pkgName);
importPackages.add(pkgName);
// 导入指定类/接口
} else {
int pi = pkg.lastIndexOf('.');
if (pi > 0) {
String pkgName = pkg.substring(0, pi);
pool.importPackage(pkgName);
importPackages.add(pkgName);
fullNames.put(pkg.substring(pi + 1), pkg);
}
}
}
String[] packages = importPackages.toArray(new String[0]);
// 匹配extends
matcher = EXTENDS_PATTERN.matcher(source);
CtClass cls;
if (matcher.find()) {
String extend = matcher.group(1).trim();
String extendClass;
// 内嵌的类,如: extends A.B
if (extend.contains(".")) {
extendClass = extend;
// 指定引用的类
} else if (fullNames.containsKey(extend)) {
extendClass = fullNames.get(extend);
// 引用整个包下的类
} else {
extendClass = ClassUtils.forName(packages, extend).getName();
}
// 创建 CtClass 对象
cls = pool.makeClass(name, pool.get(extendClass));
} else {
// 创建 CtClass 对象
cls = pool.makeClass(name);
}
// 匹配 implements
matcher = IMPLEMENTS_PATTERN.matcher(source);
if (matcher.find()) {
String[] ifaces = matcher.group(1).trim().split("\\,");
for (String iface : ifaces) {
iface = iface.trim();
String ifaceClass;
// 内嵌的接口,例如:extends A.B
if (iface.contains(".")) {
ifaceClass = iface;
// 指定引用的接口
} else if (fullNames.containsKey(iface)) {
ifaceClass = fullNames.get(iface);
// 引用整个包下的接口
} else {
ifaceClass = ClassUtils.forName(packages, iface).getName();
}
// 添加接口
cls.addInterface(pool.get(ifaceClass));
}
}
// 获得类中的内容,即 { } 内的内容
String body = source.substring(source.indexOf("{") + 1, source.length() - 1);
// 匹配 方法、属性
String[] methods = METHODS_PATTERN.split(body);
for (String method : methods) {
method = method.trim();
if (method.length() > 0) {
// 构造方法
if (method.startsWith(className)) {
cls.addConstructor(CtNewConstructor.make("public " + method, cls));
// 变量
} else if (FIELD_PATTERN.matcher(method).matches()) {
cls.addField(CtField.make("private " + method, cls));
// 方法
} else {
cls.addMethod(CtNewMethod.make("public " + method, cls));
}
}
}
// 生成类
return cls.toClass(ClassHelper.getCallerClassLoader(getClass()), JavassistCompiler.class.getProtectionDomain());
}
}
整个逻辑下来就是按照编写一个类的步骤对自适应类的字符串进行正则匹配拆解,不断通过正则表达式匹配不同部分的代码,然后调用Javassist的API生成代表不同部分的对象,最终组装成一个完整的自适应扩展类,还是挺简单的。这里说一句,dubbo中很多地方都是采用拼接字符串方式,然后通过具体的技术手段生成目标对象,如dubbo 的服务暴露源码中Wrapper类的生成逻辑也是先拼接字符串,然后通过dubbo的ClassGenerator处理成Class,但是ClassGenerator内部也是封装了Javassist相关对象,具体生成Class还是Javassist来完成的。
JdkCompiler 编译器
JdkCompiler使用的是jdk内置的编译器,主要使用三个不同功能的对象完成对字符串的编译:
- JavaFileObject对象
将字符串代码包装成一个文件对象 - JavaFileManager接口
负责管理文件的读取和输出位置 - JavaCompiler.CompilationTask 对象
把JavaFileObject对象 编译成具体的类
public Class<?> doCompile(String name, String sourceCode) throws Throwable {
int i = name.lastIndexOf('.');
String packageName = i < 0 ? "" : name.substring(0, i);
String className = i < 0 ? name : name.substring(i + 1);
// 1 创建JavaFileObject 对象
JavaFileObjectImpl javaFileObject = new JavaFileObjectImpl(className, sourceCode);
// 2 JavaFileManager 管理类文件的输入和输出位置
javaFileManager.putFileForInput(StandardLocation.SOURCE_PATH, packageName, className + ClassUtils.JAVA_EXTENSION, javaFileObject);
// 3 调用JavaCompiler.CompilationTask 的call方法 把JavaFileObject对象 编译成具体的类
Boolean result = compiler.getTask(null, javaFileManager, diagnosticCollector, options,
null, Arrays.asList(javaFileObject))
.call();
if (result == null || !result) {
throw new IllegalStateException("Compilation failed. class: " + name + ", diagnostics: " + diagnosticCollector);
}
// 加载生成的类
return classLoader.loadClass(name);
}
上面代码就是JdkCompiler编译的逻辑,使用的都是jdk的接口,想要了解更多可以自行查看源代码,其它的就不多做分析。
小结
自此,dubbo spi分析完了。dubbo框架具有良好的扩展性得益于两个方面,第一个方面就是在不同的场景中,dubbo使用了不同的设计模式,第二个方面就是dubbo spi机制。可以说dubbo中几乎所有的组件都是通过dubbo spi机制串联起来的,串联的总线就是Dubbo URL,可见dubbo spi在整个框架中的重要性。在接下来的几篇文章中我们将一起了解下dubbo多样的配置,总体上不难,就是内容有点多。