이전에 자바 컴파일러의 컴파일 단계라는 글을 쓴 적이 있다.
사실은 그 컴파일 이전에 구문 분석 트리를 만드는 작업을 수행한다.
이 글은 그 과정에 대해 다룬다.
자바에서는 JSP등에서 Runtime 중 동적 컴파일링을 할 수있는 도구를 제공한다.
ToolProvider 라는 서비스 로더를 통해 시스템 자바 컴파일러를 가져올 수 있다.
아래 코드는 자바 컴파일러를 가져오는 문장이다.
import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;
public class JavaCompilerTest {
public static void main(String[] args) {
final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
// ..
이후 JavaCompiler 라는 인터페이스를 통해 컴파일 할 수 있는 작업(task)를 획득 후, call() 메서드를 호출하면 컴파일을 할 수 있다.
대략적인 모습은 아래와 같다.
import com.naver.cafe.javachobostudy.jujitsu.StringSource;
import javax.tools.JavaCompiler;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
public class JavaCompilerTest {
public static void main(String[] args) {
final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
Iterable<JavaFileObject> sources = // ...
final JavaCompiler.CompilationTask task = compiler.getTask(
null, null, null, null, null, sources);
Boolean success = task.call();
}
}
call() 호출 결과는 Boolean 타입이며 성공, 실패를 나타낸다.
Boxed(wrapper) 타입이긴 하나 내부의 코드는 boolean이기에 null로 반환될 경우는 없다.
// JavacTaskImpl.java
package com.sun.tools.javac.api;
public class JavacTaskImpl extends BasicJavacTask {
private Result result;
// ..
public Boolean call() {
return this.doCall().isOK();
}
// Main.java
package com.sun.tools.javac.main;
public class Main {
// ..
public static enum Result {
OK(0),
ERROR(1),
CMDERR(2),
SYSERR(3),
ABNORMAL(4);
public final int exitCode;
private Result(int exitCode) {
this.exitCode = exitCode;
}
public boolean isOK() {
return this.exitCode == 0;
}
}
}
JavaCompiler는 compile 이름의 오버로딩되어 있는 메서드가 두 개 있는데 그중 인자가 많은 쪽에 구현 코드가 적혀있다.
public void compile(List<JavaFileObject> sourceFileObjects,
List<String> classnames,
Iterable<? extends Processor> processors)
{
// ..
try {
initProcessAnnotations(processors);
delegateCompiler =
processAnnotations(
enterTrees(stopIfError(CompileState.PARSE, parseFiles(sourceFileObjects))),
classnames);
delegateCompiler.compile2();
// ..
} catch (Abort ex) {
// ..
}
}
마치 PARSE 과정의 부분과 유사하게 코드가 적혀있는데, 괄호의 안쪽부터 수행된다.
1. parseFiles
2. stopIfError
3. enterTrees
4. complie2 : 이것이 지난번에 기록했던 글의 과정이다.
1. parseFiles
입력으로 받은 파일들을 parse하는 과정이다.
public List<JCCompilationUnit> parseFiles(Iterable<JavaFileObject> fileObjects) {
if (shouldStop(CompileState.PARSE))
return List.nil();
//parse all files
ListBuffer<JCCompilationUnit> trees = new ListBuffer<>();
Set<JavaFileObject> filesSoFar = new HashSet<JavaFileObject>();
for (JavaFileObject fileObject : fileObjects) {
if (!filesSoFar.contains(fileObject)) {
filesSoFar.add(fileObject);
trees.append(parse(fileObject));
}
}
return trees.toList();
}
2. stopIfError
파싱하는 과정에서 에러가 발생하면 컴파일을 해도 의미가 없기 때문에 중간에 컴파일을 종료할 수 있게 에러를 체크하는 과정이다.
protected final <T> Queue<T> stopIfError(CompileState cs, Queue<T> queue) {
return shouldStop(cs) ? new ListBuffer<T>() : queue;
}
3. enterTrees
public List<JCCompilationUnit> enterTrees(List<JCCompilationUnit> roots) {
//enter symbols for all files
if (!taskListener.isEmpty()) {
for (JCCompilationUnit unit: roots) {
TaskEvent e = new TaskEvent(TaskEvent.Kind.ENTER, unit);
taskListener.started(e);
}
}
enter.main(roots);
if (!taskListener.isEmpty()) {
for (JCCompilationUnit unit: roots) {
TaskEvent e = new TaskEvent(TaskEvent.Kind.ENTER, unit);
taskListener.finished(e);
}
}
// If generating source, or if tracking public apis,
// then remember the classes declared in
// the original compilation units listed on the command line.
if (needRootClasses || sourceOutput || stubOutput) {
ListBuffer<JCClassDecl> cdefs = new ListBuffer<>();
for (JCCompilationUnit unit : roots) {
for (List<JCTree> defs = unit.defs;
defs.nonEmpty();
defs = defs.tail) {
if (defs.head instanceof JCClassDecl)
cdefs.append((JCClassDecl)defs.head);
}
}
rootClasses = cdefs.toList();
}
// Ensure the input files have been recorded. Although this is normally
// done by readSource, it may not have been done if the trees were read
// in a prior round of annotation processing, and the trees have been
// cleaned and are being reused.
for (JCCompilationUnit unit : roots) {
inputFiles.add(unit.sourcefile);
}
return roots;
}
크게 보면 간단해 보이지만 실제 간단하지 않다.
1-1. parse
위에서 parseFiles안의 작업은 실제 parse라는 메서드에 의해 수행된다.
public JCTree.JCCompilationUnit parse(JavaFileObject filename) {
JavaFileObject prev = log.useSource(filename);
try {
JCTree.JCCompilationUnit t = parse(filename, readSource(filename));
if (t.endPositions != null)
log.setEndPosTable(filename, t.endPositions);
return t;
} finally {
log.useSource(prev);
}
}
protected JCCompilationUnit parse(JavaFileObject filename, CharSequence content) {
long msec = now();
JCCompilationUnit tree = make.TopLevel(List.<JCTree.JCAnnotation>nil(),
null, List.<JCTree>nil());
if (content != null) {
if (verbose) {
log.printVerbose("parsing.started", filename);
}
if (!taskListener.isEmpty()) {
TaskEvent e = new TaskEvent(TaskEvent.Kind.PARSE, filename);
taskListener.started(e);
keepComments = true;
genEndPos = true;
}
Parser parser = parserFactory.newParser(content, keepComments(), genEndPos, lineDebugInfo);
tree = parser.parseCompilationUnit();
if (verbose) {
log.printVerbose("parsing.done", Long.toString(elapsed(msec)));
}
}
tree.sourcefile = filename;
if (content != null && !taskListener.isEmpty()) {
TaskEvent e = new TaskEvent(TaskEvent.Kind.PARSE, tree);
taskListener.finished(e);
}
return tree;
}
'Programing > JVM(Java, Kotlin)' 카테고리의 다른 글
[Java] hashCode() internal : String, Object (0) | 2020.01.09 |
---|---|
[Java] 한 영역(scope)에서 변수를 두 번 선언할 수 없는 이유? (1) | 2019.11.23 |
[Java] Effective Java : Item 36 EnumSet, 유스케이스 in Java 8 (0) | 2019.10.27 |
[Java] Stream in depth : Stream & Pipeline (0) | 2019.10.27 |
[Sonarqube] Functional Interfaces should be as specialised as possible (0) | 2019.10.18 |