java之徒手实现源码的动态生成,编译以及加载
Robit低代码平台在去年的时候进一步改造升级,确定了用户设计的应用必须生成可阅读可二次开发的源码。考虑到当前平台使用的技术栈,确定应用源码的前端依旧使用Vue,后端使用spring boot。
在改造生成应用源码之前,我们需要先技术选型,确定如何生成源码。在考虑这个问题之前,平台当时也做过类似的需求,不过那个时候是生成实体和结构体的源码,因为代码结构比较简单直接使用字符串拼接的方式生成的。
核心代码如下,比较特殊的是实体的属性都是public类型且又配置了get和set方法,因为低代码平台允许用户使用is开头的bool类型值的属性名,这种命名可能导致序列化反序列丢失值,所以属性都改成public,且配置了
jackson的注解@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.PUBLIC_ONLY, getterVisibility = JsonAutoDetect.Visibility.NONE)
,直接把get方法屏蔽掉。
public String build(String clazzName) {
// 构建头部
fileContent.append("package ").append(packageName).append(";\n\n");
imports.add("import com.fasterxml.jackson.annotation.JsonAutoDetect;\n");
for (String anImport : imports) {
fileContent.append(anImport);
}
fileContent.append(
"@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.PUBLIC_ONLY, getterVisibility = JsonAutoDetect.Visibility.NONE)\n");
fileContent.append("public class ").append(clazzName).append(" {\n");
fileContent.append("public ").append(clazzName).append("(){}\n\n");
fileContent.append(" private Set<String> changedFields = new HashSet<>();\n");
// 构建属性
props.forEach((propertyName, className) -> {
String content = MessageFormat.format(" public {1} {0};\n\n", propertyName, className);
fileContent.append(content);
});
// 构建 getter setter
props.forEach((propertyName, className) -> {
String fixedName = propertyName.substring(0, 1).toUpperCase(Locale.ROOT) + propertyName.substring(1);
String content = MessageFormat.format(
" public {2} get{1}() '{'\n"
+ " return {0};\n"
+ " '}'\n"
+ " public void set{1}({2} {0}) '{'\n"
+ " this.{0} = {0};\n"
+ " '}'\n", propertyName, fixedName, className);
fileContent.append(content);
});
// 构建ToMap方法
fileContent.append(" public Map<String,Object> toMap(){\n"
+ " Map<String,Object> toMap = new HashMap<>();\n");
props.forEach((column, type) ->
fileContent.append(
(" if(changedFields.contains(\"_NAME\") || \"id\".equals(\"_NAME\") ) {\n"
+ " toMap.put(\"_NAME\",_NAME);\n"
+ " }\n")
.replaceAll("_NAME", column)));
fileContent.append(
" return toMap;\n"
+ " }\n\n");
// 构建尾部
return fileContent.append("}\n").toString();
}
然后调用jdk自带的JavaCompiler,实际调用的是javac执行的编译,然后从生成的class文件读取内容存入数据库。在很久之前jdk内置的工具都改成了java实现,那些exe文件其实都是空壳文件,文件大小都是24k。
compiler.run方法的前三个参数分别是标准输入流,标准输出流,标准错误输出流,可以通过这三个参数细化编译,比如记录编译日志。 run方法的返回值为0的时候表示成功,其他值都是失败。为什么返回值是个数字呢?并且编译失败还不抛异常? 很简单,这个是调用javac这个命令行程序的代码实现的,哪来的java异常。。。返回值是个数字也很简单,这个值就是标准的程序退出状态码。
核心代码中不考虑这些复杂问题,我们只需要查看是否存在class文件即可,不存在就是编译出错了。本方法返回的字节数组或base64编码后插入数据库。
public byte[] compiler(String name) {
String javaPackageName = name.replace(".", File.separator) + ".java";
String javaAbsolutePath = javaFileHome + javaPackageName;
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
compiler.run(null, null, null, "-encoding", "UTF-8", "-classpath", javaFileHome, javaAbsolutePath);
String classFile = javaFileHome + name.replace(".", File.separator) + ".class";
try {
return Files.readAllBytes(new File(classFile).toPath());
} catch (IOException exception) {
throw new LogicCompileException("类编译失败", exception);
}
}
自定义的classload核心代码如下,类继承自ClassLoader,ClassLoader已经实现了加载类的全部给你了,我继承它只要为了修改读取类文件的方式,需要从数据库里读取。 这个类实现了核心的加载类定义的功能。具体的说明我写到代码的注释里了。
public class EntityClassLoader extends ClassLoader {
// 读取编译的类的数据 mapper
private final ClassCacheMapper mapper;
public EntityClassLoader(ClassCacheMapper mapper) {
// 使用spring的 classloader 作为父加载器
super(ClassUtils.getDefaultClassLoader());
this.mapper = mapper;
}
// 加载类的方法 没有缓存情况下调用
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data;
String[] splitName = name.split("\\.");
data = loadClassBytes(splitName[splitName.length - 1]);
// 直接把数据喂给defineClass方法
return defineClass(name, data, 0, data.length);
}
// 读取类定义的方法 如果缓存内加载不到,会内部调用findClass执行查找
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 补全类的完整包名
String fixedName = name.contains(".") ? name : "cn.xxx.logic.custom._"
+ CurrentTenantIdentifierResolverImpl.getTenantIdStatic().replace("-", "_") + "." + name;
// 基于双亲委派原则 否则类中使用的String Integer之类的非手动生成的类是无法加载的
return super.loadClass(fixedName);
}
// 核心代码 就是个select操作 哈哈哈
private byte[] loadClassBytes(String name) throws ClassNotFoundException {
String result = mapper.find(name);
if (result == null || result.length() == 0) {
throw new ClassNotFoundException("class " + name + " not found in database");
}
// base64解码
return Base64.decodeBase64(result);
}
}
通过以上的代码,我们是无法通过编译步骤的,因为最开始的生成源码的代码中确定了生成的源码里使用jackson的注解,编译的时候会报错提示找不到这些注解。
public void copyJsonAutoDetect() throws IOException {
copyFile("JsonAutoDetect$1.class");
copyFile("JsonAutoDetect$Value.class");
copyFile("JsonAutoDetect$Visibility.class");
copyFile("JsonAutoDetect.class");
}
我们需要把这几个文件拷贝到编译历史目录内才行。