Java实现动态编译加载外部类,适用于动态模板生成java文件编译执行场景

  最近工作中遇到一个业务场景:通过编辑器编辑html及java代码保存到文件中,类似传统的jsp方式只不过文件名为html,文件编辑完成后需要动态将该文件编译运行生成html内容并保存到文件中。

首先整理一下思路:

  1.可执行java代码是在html中嵌套的;

  2.文件是html类型的文件

  3.需要将html文件以java class方式运行并返回结果

根据以上确定大致方案

  1. 需要借鉴jsp解析方式将html内容另存为java文件格式

  2. 动态编译该java文件生成class文件

  3. 动态加载class并初始化,通过反射方式执行方法

此处也存在几个问题,html内容中并没有可执行方法,我们加载class并初始化后如何反射方式执行;执行后的html内容如何获取

1. 首先我们定义接收html的容器,可以使用类似 buffer 或者 stream等可以字符串的对象

2.改写html内容保存java文件,模板jsp解析,将html输出到第一步定义的容器中,如果是java代码同样执行并输出到第一步定义的容器中,改写后的java文件需要有一个暴漏的方法及一个入参,这个参数就是我们第一步定义的容器

3.动态编译该java文件生成class文件

4.动态加载class并初始化,通过反射方式执行第二步暴漏的方法,入参为第一步定义的容器

 

下面主要介绍如何动态编译加载并执行

这一步google上面有很多例子,比如创建自定义classloader动态编译执行巴拉巴拉。。。。。。

之前有接触过阿里的arthas,了解到它可以实现动态编译加载,既然别人都造好了轮子哪有不使用的道理呢。当然我们需要做一些改造,arthas是基于agent方式启动我们项目中肯定不希望通过这种方式,而且arthas包含模块较多,直接照搬过来也有点浪费,所以我们只挑选自己能用到的模块,既然我们要实现动态编译和加载,那我们只需要把编译模块和动态加载模块拿过来就行了

<dependency>
                <groupId>com.taobao.arthas</groupId>
                <artifactId>arthas-memorycompiler</artifactId>
                <version>3.6.0</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>repackage-asm</artifactId>
                <version>0.0.13</version>
            </dependency>

我们没有通过agent方式来使用,但是arthas是依赖于agent启动时安装的Instrumentation , 我们需要通过代码方式嵌入

//无论是web服务或者微服务,都可以在服务启动后注册安装,然后我们就可以放心使用arthas了
ByteBuddyAgent.install();

下面我们就可以阅读arthas源码来实现自己的动态编辑加载了,首先我们看arthas如何做动态编译:

// MemoryCompilerCommand 类 下对应动态变异的处理
ClassLoader classloader = null; if (hashCode == null) { classloader = ClassLoader.getSystemClassLoader(); } else { classloader = ClassLoaderUtils.getClassLoader(inst, hashCode); if (classloader == null) { process.end(-1, "Can not find classloader with hashCode: " + hashCode + "."); return; } } DynamicCompiler dynamicCompiler = new DynamicCompiler(classloader); Charset charset = Charset.defaultCharset(); if (encoding != null) { charset = Charset.forName(encoding); } for (String sourceFile : sourcefiles) { String sourceCode = FileUtils.readFileToString(new File(sourceFile), charset); String name = new File(sourceFile).getName(); if (name.endsWith(".java")) { name = name.substring(0, name.length() - ".java".length()); } dynamicCompiler.addSource(name, sourceCode); } Map<String, byte[]> byteCodes = dynamicCompiler.buildByteCodes(); File outputDir = null; if (this.directory != null) { outputDir = new File(this.directory); } else { outputDir = new File("").getAbsoluteFile(); } List<String> files = new ArrayList<String>(); for (Entry<String, byte[]> entry : byteCodes.entrySet()) { File byteCodeFile = new File(outputDir, entry.getKey().replace('.', '/') + ".class"); FileUtils.writeByteArrayToFile(byteCodeFile, entry.getValue()); files.add(byteCodeFile.getAbsolutePath()); affect.rCnt(1); }

这里有很多arthas的监控及统计,我们将无用代码剔除后只剩下关键代码即可,大概代码如下

/**
     * 编译
     *
     * @param javaFile
     * @param charSet
     * @return
     * @throws Exception
     */
    public String compiler(DynamicCompiler dynamicCompiler, String parentPath, String javaFile, String charSet) throws Exception {
        File file = new File(parentPath, javaFile);
        String sourceCode = FileUtils.readFileToString(file, charSet);
        String name = file.getName();
        if (name.endsWith(".java")) {
            name = name.substring(0, name.length() - ".java".length());
        }
        dynamicCompiler.addSource(name, sourceCode);
        Map<String, byte[]> byteCodes = dynamicCompiler.buildByteCodes();

        String className = null;
        for (Map.Entry<String, byte[]> entry : byteCodes.entrySet()) {
            className = entry.getKey();
            File byteCodeFile = new File(parentPath, entry.getKey().replace('.', '/').concat(".class"));
            FileUtils.writeByteArrayToFile(byteCodeFile, entry.getValue());
        }
        return className;
    }

通过上面实现了java文件动态编译为class文件后,下面我们在看如何动态加载执行class


// ClassLoaderCommand 类下的动态加载处理
private void processLoadClass(CommandProcess process, Instrumentation inst, ClassLoader targetClassLoader) {
        if (targetClassLoader != null) {
            try {
                Class<?> clazz = targetClassLoader.loadClass(this.loadClass);
                process.appendResult(new MessageModel("load class success."));
                ClassDetailVO classInfo = ClassUtils.createClassInfo(clazz, false);
                process.appendResult(new ClassLoaderModel().setLoadClass(classInfo));

            } catch (Throwable e) {
                logger.warn("load class error, class: {}", this.loadClass, e);
                process.end(-1, "load class error, class: "+this.loadClass+", error: "+e.toString());
                return;
            }
        }
        process.end();
    }

其实这里只有一句关键代码 targetClassLoader.loadClass(this.loadClass) ,其核心就是这个classloader上面

private Class<?> loadTargetClass(DynamicCompiler dynamicCompiler, String className) throws Exception {
        return dynamicCompiler.getClassLoader().loadClass(className);
    }

执行完上面的基本可以实现编译和加载,接着我们执行初始化并运行

 // 载入类
        try {
            templateObject = (BaseTemplate) clazz.newInstance();
        } catch (Exception e) {
            log.error("初始化模板出错:{}", e.getMessage(), e);
            templateSystem.response.output("初始化模板出现了错误,详细请看以上信息!" + e.getMessage());
            throw new BizException(ErrorCodeEnum.GENERATOR_ERROR, templateSystem.response.outputBuffer());
        }
        try {
            templateObject.service(templateSystem);
        } catch (Exception e) {
            log.error("执行模板文件失败:{}", e.getMessage(), e);
        }
templateSystem中包含接受html输出的容器,这样我们就能获取到执行后的输出内容

 到闲着我们已经完成了动态编译和加载运行并获取结果,但当模板内容变化该如何让其重新加载呢,这个arthas也提供了对应的处理

 Map<String, byte[]> bytesMap = new HashMap<String, byte[]>();
        for (String path : paths) {
            RandomAccessFile f = null;
            try {
                f = new RandomAccessFile(path, "r");
                final byte[] bytes = new byte[(int) f.length()];
                f.readFully(bytes);

                final String clazzName = readClassName(bytes);

                bytesMap.put(clazzName, bytes);

            } catch (Exception e) {
                logger.warn("load class file failed: "+path, e);
                process.end(-1, "load class file failed: " +path+", error: " + e);
                return;
            } finally {
                if (f != null) {
                    try {
                        f.close();
                    } catch (IOException e) {
                        // ignore
                    }
                }
            }
        }

.......
try {
            if (definitions.isEmpty()) {
                process.end(-1, "These classes are not found in the JVM and may not be loaded: " + bytesMap.keySet());
                return;
            }
            inst.redefineClasses(definitions.toArray(new ClassDefinition[0])); //这里是核心,调用安装的 Intsrumentation 进行class的redefine
            process.appendResult(redefineModel);
            process.end();
        } catch (Throwable e) {
            String message = "redefine error! " + e.toString();
            logger.error(message, e);
            process.end(-1, message);
        }

参考arthas上面的处理我们实现自己的动态替换

/**
     * redefine
     *
     * @param clazz
     * @param classFile
     */
    private void redefine(Class<?> clazz, String classFile) throws Exception {
        RandomAccessFile f = null;
        ClassDefinition definition = null;
        try {
            f = new RandomAccessFile(classFile, "r");
            final byte[] bytes = new byte[(int) f.length()];
            f.readFully(bytes);

            definition = new ClassDefinition(clazz, bytes);
        } catch (Exception e) {
            log.error("load class file {} failed {}", classFile, e.getMessage(), e);
        } finally {
            if (f != null) {
                try {
                    f.close();
                } catch (IOException e) {
                }
            }
        }
        getInstrumentation().redefineClasses(definition);
    }

最后我们看下所有的执行流程

Class<?> clazz = null;
//判断是否需要重新编译
if (needCompile(outputClass, physicalTemplate)) { try { parse(templateContent, physicalTemplate, pathInfo.className, outputFile, pathInfo._package, charSet); } catch (Exception e) { log.error("生成模板出现了错误,{}", e.getMessage(), e); templateSystem.response.output("生成模板出现了错误,详细请看以上信息!" + e.toString()); throw new BizException(ErrorCodeEnum.GENERATOR_ERROR, templateSystem.response.outputBuffer()); } try { clazz = compilerLoadClass(pathInfo, charSet); //redefine redefine(clazz, outputClass); } catch (Exception e) { log.error("编译模板出现了错误,{}", e.getMessage(), e); templateSystem.response.output("编译模板出现了错误,详细请看以上信息!" + e.toString()); throw new BizException(ErrorCodeEnum.GENERATOR_ERROR, templateSystem.response.outputBuffer()); } } else { clazz = getLoadClass(pathInfo.clazz); if (clazz == null) { try { clazz = compilerLoadClass(pathInfo, charSet); } catch (Exception e) { log.error("编译模板出现了错误,{}", e.getMessage(), e); templateSystem.response.output("编译模板出现了错误,详细请看以上信息!" + e.toString()); throw new BizException(ErrorCodeEnum.GENERATOR_ERROR, templateSystem.response.outputBuffer()); } } } // 载入类 try { templateObject = (BaseTemplate) clazz.newInstance(); } catch (Exception e) { log.error("初始化模板出错:{}", e.getMessage(), e); templateSystem.response.output("初始化模板出现了错误,详细请看以上信息!" + e.getMessage()); throw new BizException(ErrorCodeEnum.GENERATOR_ERROR, templateSystem.response.outputBuffer()); } try { templateObject.service(templateSystem); } catch (Exception e) { log.error("执行模板文件失败:{}", e.getMessage(), e); }

 

posted @ 2023-06-20 19:56  山顶上的蜗牛  阅读(1198)  评论(0)    收藏  举报