前言
使用过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()) {