java动态编译——JavaFileManager详解

本文详细探讨了Java动态编译过程中JavaFileManager的重要性,通过分析Arthas的内存编译实现,揭示了如何处理依赖问题,以及自定义JavaFileManager以满足特定需求。通过重写关键方法,如list、getClassLoader和inferBinaryName,可以实现如内存中保存编译结果等功能。
摘要由CSDN通过智能技术生成
前言

使用过java内存编译功能的小伙伴应该了解,我们可以通过tools包提供的JavaCompiler模块在内存中对java代码进行编译,而我们经常使用的javac编译工具,底层也是借助tools.jar完成编译的。在java代码里,我们可以通过下面几行代码,就完成源文件到字节码的编译:

        JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();

        StandardJavaFileManager fileManager = javaCompiler.getStandardFileManager(null, null, null);
        Iterable<? extends JavaFileObject> fileObjects = fileManager.getJavaFileObjects(sourceCodeFilePath);
        // 指定编译的class文件的路径
        List<String> options = Arrays.asList("-d", compilePath);
        JavaCompiler.CompilationTask cTask = javaCompiler.getTask(null, fileManager, null, options, null, fileObjects);
        Boolean success = cTask.call();

        if (success) {
   
            // 从source code中读出包名
            File sourceFile = new File(sourceCodeFilePath);
            BufferedReader bufferedReader = new BufferedReader(new FileReader(sourceFile));
            String line;
            while ((line = bufferedReader.readLine()) != null) {
   
                if (line.startsWith("package")) {
   
                    break;
                }
            }

            String packageName = line;
            packageName = packageName.replaceAll("package", "").trim().replaceAll("\\.", "/");
            packageName = packageName.substring(0, packageName.length() - 1);

            // 获取类名(这里简单处理, 默认文件名与类名相同)
            String sourceFileName = sourceFile.getName();

            String[] split = sourceFileName.split("\\.");
            String simpleClassName = split[0] + "." + "class";

            return compilePath + "/" +  packageName + "/" + simpleClassName;
        }

编译完成的class文件。会被放在-d选项指定的目录下(保留包名),我们可以通过读取文件中保存的字节码做类的动态加载,动态增强等一系列看上去很酷的操作。但是通过javaCompiler的getStandardFileManager获取的默认JavaFileManager,可能无法满足我们的所有需求,比如我们想完全指定编译结果的输出, 比如不生成文件而是直接保存在内存字节数组中直接使用,而当编译出现问题时,往往都和使用的JavaFileManager有关,这时候排查问题就需要我们对其了解,而实际上网络上相关的有价值内容甚少,所以才有这篇博客。

从问题开始

对于JavacFileManager的探究起源于开发远端热部署功能遇到的问题,最开始的设计思路是在在本地对修改后的java文件进行编译,然后将编译好的class文件上传至服务器,通过agent机制加载更新后的字节码进行redefine,但是其它步骤都通过验证后,最终卡在了编译这一步,我发现用前言中贴出的那段代码,对毫无依赖的简单java文件(实际上最初测试内存编译时使用的就是最常见的HelloWorld)可以编译成功,但是一旦有依赖关系,编译就会失败,并提示:

line: 5 , message: 找不到符号
  符号:   类 xxx
  位置: 程序包 xxx.xxx.xxx
...  

有点眼熟,我们使用javac命令进行编译时也会碰到一样的提示,因为实际上底层都是通过tools包进行的编译,而javac命令提供了-classpath 选项在依赖找不到时手动指定classpath,但是这个指定的classpath会覆盖默认的classpath,且在我的使用场景下,在外部获取需要热部署的程序的classpath并不是一件轻松的事情,而之前使用arthas的经验告诉我,带依赖的内存编译是可行的,并且似乎可以不自己指定classpath,那么就clone下arthas的源码,找寻答案

arthas的内存编译指令是mc,那我们从指令入口开始,首先定位到MemoryCompilerCommand指令类,接下来分析类的process指令处理方法:

    @Override
    public void process(final CommandProcess process) {
   
        int exitCode = 0;
        RowAffect affect = new RowAffect();

        try {
   
            Instrumentation inst = process.session().getInstrumentation();
            ClassLoader classloader = null;
            if (hashCode == null) {
   
                classloader = ClassLoader.getSystemClassLoader();
            } else {
   
                classloader = ClassLoaderUtils.getClassLoader(inst, hashCode);
                if (classloader == null) {
   
                    process.write("Can not find classloader with hashCode: " + hashCode + ".\n");
                    exitCode = -1;
                    return;
                }
            }

            DynamicCompiler dynamicCompiler = new DynamicCompiler(classloader, new Writer() {
   
                @Override
                public void write(char[] cbuf, int off, int len) throws IOException {
   
                    process.write(new String(cbuf, off, len));
                }

                @Override
                public void flush() throws IOException {
   
                }

                @Override
                public void close() throws IOException {
   

                }

            });

            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();
            }

            process.write("Memory compiler output:\n");
            for (Entry<String, byte[]> entry : byteCodes.entrySet()) {
   
                File byteCodeFile = new File(outputDir, entry.getKey().replace('.', '/') + ".class");
                FileUtils.writeByteArrayToFile(byteCodeFile, entry.getValue());
                process.write(byteCodeFile.getAbsolutePath() + '\n');
                affect.rCnt(1);
            }

        } catch (Throwable e) {
   
            logger.warn("Memory compiler error", e);
            process.write("Memory compiler error, exception message: " + e.getMessage()
                            + ", please check $HOME/logs/arthas/arthas.log for more details. \n");
            exitCode = -1;
        } finally {
   
            process.write(affect + "\n");
            process.end(exitCode);
        }
    }

arthas自己实现了一个DynamicCompile,并且看上去”只需要“将读取的源文件的内容通过addSource方法添加到DynamicCompile中再执行buildByteCodes方法,就可以得到对应的编译结果。那我们重点分析DynamicCompile类

private final JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
    private final StandardJavaFileManager standardFileManager;
    private final List<String> options = new ArrayList<String>();
    private final DynamicClassLoader dynamicClassLoader;

    private final Collection<JavaFileObject> compilationUnits = new ArrayList<JavaFileObject>();
    private final List<Diagnostic<? extends JavaFileObject>> errors = new ArrayList<Diagnostic<? extends JavaFileObject>>();
    private final List<Diagnostic<? extends JavaFileObject>> warnings = new ArrayList<Diagnostic<? extends JavaFileObject>>();

    private Writer writer;

    public DynamicCompiler(ClassLoader classLoader) {
   
        this(classLoader, null);
    }

    public DynamicCompiler(ClassLoader classLoader, Writer writer) {
   
        standardFileManager = javaCompiler.getStandardFileManager(null, null, null);

        options.add("-Xlint:unchecked");
        dynamicClassLoader = new DynamicClassLoader(classLoader);
        this.writer = writer;
    }

    public void addSource(String className, String source) {
   
        addSource(new StringSource(className, source));
    }

    public void addSource(JavaFileObject javaFileObject) {
   
        compilationUnits.add(javaFileObject);
    }
    
    public Map<String, byte[]> buildByteCodes() {
   

        errors.clear();
        warnings.clear();

        JavaFileManager fileManager = new DynamicJavaFileManager(standardFileManager, dynamicClassLoader);

        DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<JavaFileObject>();
        JavaCompiler.CompilationTask task = javaCompiler.getTask(null, fileManager, collector, options, null,
                        compilationUnits);

        try {
   

            if (!compilationUnits.isEmpty()) {
   
                boolean result = task.call();

                if (!result || collector.getDiagnostics().size() > 0) {
   

                    for (Diagnostic<? extends JavaFileObject> diagnostic : collector.getDiagnostics()) {
   
                
  • 9
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 19
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值