前端编译器把java文件编译成class文件,如:JDK的Javac、Eclipse JDT中的增量式编译器(ECJ)
《Java虚拟机规范》未规定如何将java文件编译为Class文件,给予了较大的灵活性
编译过程与具体编译器实现绑定,可能会导致某些代码在Javac中可以编译,而在ECJ则不可编译
Javac编译器由纯Java实现
下面导入JDK8中的Javac源码,新建项目,将源码langtools/src/share/classes中的com/sun复制到项目目录(根据提示将private改为package即可解决报错)

将Run-RunConfiguration-Main中的Mian Class指定为javac.Main

新建一个Test.java,在Argument-Program argument指定其路径

点击Run,即可编译出Test.class,与在命令行中运行javac Test.java效果是一样的


Javac编译动作的入口是
com.sun.tools.javac.main.JavaCompiler类,关键代码如下

解析由parseFiles()方法完成,包括词法分析和语法分析
词法分析将源代码的字符流转变为标记(Token)集合
语法分析根据标记序列构造抽象语法树
如下是Eclipse AST View插件分析出来的某段代码的抽象语法树视图

生成语法树以后,编译器不会再对源码字符流进行操作了,后续操作都建立在抽象语法树之上
填充符号表由enterTrees()方法完成,符号表(Symbol Table)是由一组符号地址和符号信息构成的数据结构
注解处理由processAnnotations()方法完成,JDK6之前注解只会在程序运行时发挥作用,JDK6设计了插入式注解处理器API将注解处理提前至编译期
可把插入式注解处理器看作编译器的插件,允许读取、修改、添加抽象语法树中的任意元素,如注解来实现自动产生getter/setter方法、生成受查异常表等
若在处理注解期间修改过语法树,编译器将回到解析及填充符号表的过程重新处理,直至不再修改语法树,循环过程称为一个轮次(Round)
若有新的注解处理器,会通过com.sun.tools.javac.processing.JavacProcessing-Environment类的doProcessing()方法生成一个新的JavaCompiler对象
分析和字节码生成由Compile2()方法完成,包括标注检查、数据流及控制流分析、解语法糖、字节码生成

分析指语义分析,对结构上正确的源程序进行上下文相关性质的检查
抽象语法树能够表示一个结构正确的源程序,但无法保证源程序的语义是符合逻辑的,如下
int a = 1;
boolean b = false;
char c = 2;
可能出现
int d = a + c;
int d = b + c;
char d = a + c;
上面3种赋值都能构成正确的抽象语法树,但只有第一种赋值在Java语义上是正确的(IDE的红线错误提示即来源于语义分析)
标注检查由attribute()完成,实现类是com.sun.tools.javac.comp.Attr和com.sun.tools.javac.comp.Check
标注检查的内容包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配
在标注检查中,会进行一个称为常量折叠(Constant Folding)的代码优化,如对于
int a = 1 + 2;
在抽象语法树上仍然能看到字面量“1”“2”和操作符“+”号,但是在经过常量折叠优化之后,会变成字面量“3”,
数据流及控制流分析由flow()方法完成,实现类为com.sun.tools.javac.comp.Flow
对程序上下文逻辑更进一步的验证,如程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值等
编译时和类加载时的数据及控制流分析基本一样,但校验范围有所区别,有一些校验项只能在编译期或运行期进行
public void foo(final int arg) {
final int var = 0;
}
public void foo(int arg) {
int var = 0;
}
如上,一个方法有final,一个没有,但它们编译出来的指令都是一模一样
解语法糖由desugar()方法完成,实现类为com.sun.tools.javac.comp.TransTypes和com.sun.tools.javac.comp.Lower
语法糖指在计算机语言中添加的某种语法,其对语言的编译结果和功能并没有实际影响,但是却能更方便使用该语言
Java中的语法糖如泛型、变长参数、自动装箱拆箱等,JVM运行时并不直接支持这些语法,需在编译阶段将其还原回原始的基础语法结构,称为解语法糖
字节码生成由generate()方法完成,实现类为com.sun.tools.javac.jvm.Gen
把前面各个步骤所生成的信息(语法树、符号表)转化成字节码指令写到磁盘中,还进行了代码添加(
完成对语法树的遍历和调整之后,就会把的符号表交到com.sun.tools.javac.jvm.ClassWriter的writeClass()方法输出字节码,生成Class文件
泛型将操作的数据类型指定为方法签名中的一种特殊参数,用于类、接口和方法的创建中,分别构成泛型类、泛型接口和泛型方法
C#是具现化式泛型(Reified Generics)
Java是类型擦除式泛型(Type Erasure Generics)
Java泛型使用效果(无法instanceof、创建对象和数组)和运行效率(需不断拆箱装箱)都不如C#,唯一优势在于实现只需要修改Javac,不需要改动字节码和JVM,也保证了以前没有使用泛型的库也可运行在JDK5之上
为了保证兼容性,定义裸类型为所有该类型泛型化实例的共同父类型(Super Type),如下
ArrayList ilist = new ArrayList();
ArrayList slist = new ArrayList();
ArrayList list; // 裸类型
list = ilist;
list = slist;
在编译时把ArrayList
public static void main(String[] args) {
Map map = new HashMap();
map.put("hello", "你好");
map.put("how are you?", "吃了没?");
System.out.println(map.get("hello"));
System.out.println(map.get("how are you?"));
}
对于如上代码,编译成Class,再反编译,程序变回泛型出现之前的写法,泛型都变回了裸类型,在元素访问时插入强制转型代码
public static void main(String[] args) {
Map map = new HashMap();
map.put("hello", "你好");
map.put("how are you?", "吃了没?");
System.out.println((String) map.get("hello"));
System.out.println((String) map.get("how are you?"));
}
泛型擦除无法支持原始类型,如下
ArrayList list = new ArrayList();
导致只能使用包装类型,运行速度慢
ArrayList list = new ArrayList();
运行时无法获取泛型类型信息
if (item instanceof E) { // 不合法,无法对泛型进行实例判断
}
E newItem = new E(); // 不合法,无法使用泛型创建对象
E[] itemArray = new E[10]; // 不合法,无法使用泛型创建数组
如写一个泛型版本的从List到数组的转换方法,需显式传递数组的组件类型componentType
public static T[] convert(List list, Class componentType) {
T[] array = (T[])Array.newInstance(componentType, list.size());
}
为了在反射中获取参数化类型,新增Signature属性用于存储一个方法在字节码层面的特征签名,其包含了参数化类型的信息
如下,包含泛型、自动装箱、自动拆箱、遍历循环与变长参数5种语法糖
public static void main(String[] args) {
List list = Arrays.asList(1, 2, 3, 4);
int sum = 0;
for (int i : list) {
sum += i;
}
System.out.println(sum);
}
反编译后
public static void main(String[] args) {
List list = Arrays.asList( new Integer[] {
Integer.valueOf(1),
Integer.valueOf(2),
Integer.valueOf(3),
Integer.valueOf(4) });
int sum = 0;
for (Iterator localIterator = list.iterator(); localIterator.hasNext(); ) {
int i = ((Integer)localIterator.next()).intValue();
sum += i;
}
System.out.println(sum);
}
使用条件为常量的if语句,会触发条件编译,如下
public static void main(String[] args) {
if (true) {
System.out.println("block 1");
} else {
System.out.println("block 2");
}
}
在编译时,就会优化
public static void main(String[] args) {
System.out.println("block 1");
}
若非if语句,则可能在控制流分析中提示错误且拒绝编译,如下
public static void main(String[] args) {
// 编译器将会提示“Unreachable code”
while (false) {
System.out.println("");
}
}
在前端编译这部分,可通过插入式注解处理器API影响Java编译子系统的行为
接下来基于插入式注解处理器API编写一个校验工具NameCheckProcessor规范书写格式:
继承抽象类javax.annotation.processing.AbstractProcessor,实现抽象方法process(),其是Javac在执行注解处理器代码时的回调,如下,把当前轮次中的每一个RootElement传递到NameChecker执行名称检查逻辑
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
@SupportedAnnotationTypes("*") //设置要处理的注解
@SupportedSourceVersion(SourceVersion.RELEASE_6) //设置处理的Java版本
public class NameCheckProcessor extends AbstractProcessor {
private NameChecker nameChecker;
@Override
public void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
nameChecker = new NameChecker(processingEnv);
}
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (!roundEnv.processingOver()) {
for (Element element : roundEnv.getRootElements())
nameChecker.checkNames(element);
}
return false; //不需要改变或添加抽象语法树,返回false,编译器不构造新的JavaCompiler实例
}
}
import java.util.EnumSet;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.ElementScanner6;
import javax.tools.Diagnostic;
public class NameChecker {
private final Messager messager;
NameCheckScanner nameCheckScanner = new NameCheckScanner();
NameChecker(ProcessingEnvironment processsingEnv) {
this.messager = processsingEnv.getMessager();
}
public void checkNames(Element element) {
nameCheckScanner.scan(element);
}
/**
* 名称检查器实现类,继承了JDK 6中的ElementScanner6
* 将会以Visitor模式访问抽象语法树中的元素
*/
private class NameCheckScanner extends ElementScanner6 {
/**
* 此方法用于检查Java类
*/
@Override
public Void visitType(TypeElement e, Void p) {
scan(e.getTypeParameters(), p);
checkCamelCase(e, true);
super.visitType(e, p);
return null;
}
/**
* 检查方法命名是否合法
*/
@Override
public Void visitExecutable(ExecutableElement e, Void p) {
if (e.getKind() == ElementKind.METHOD) {
Name name = e.getSimpleName();
if (name.contentEquals(e.getEnclosingElement().getSimpleName()))
messager.printMessage(Diagnostic.Kind.WARNING, "一个普通方法 “" + name + "”不应当与类名重复,避免与构造函数产生混淆", e);
checkCamelCase(e, false);
}
super.visitExecutable(e, p);
return null;
}
/**
* 检查变量命名是否合法
*/
@Override
public Void visitVariable(VariableElement e, Void p) {
// 如果这个Variable是枚举或常量,则按大写命名检查,否则按照驼式命名法规则检查
if (e.getKind() == ElementKind.ENUM_CONSTANT || e.getConstantValue() != null || heuristicallyConstant(e))
checkAllCaps(e);
else
checkCamelCase(e, false);
return null;
}
/**
* 判断一个变量是否是常量
*/
private boolean heuristicallyConstant(VariableElement e) {
if (e.getEnclosingElement().getKind() == ElementKind.INTERFACE)
return true;
else if (e.getKind() == ElementKind.FIELD && e.getModifiers().containsAll(EnumSet.of(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)))
return true;
else {
return false;
}
}
/**
* 检查传入的Element是否符合驼式命名法,如果不符合,则输出警告信息
*/
private void checkCamelCase(Element e, boolean initialCaps) {
String name = e.getSimpleName().toString();
boolean previousUpper = false;
boolean conventional = true;
int firstCodePoint = name.codePointAt(0);
if (Character.isUpperCase(firstCodePoint)) {
previousUpper = true;
if (!initialCaps) {
messager.printMessage(Diagnostic.Kind.WARNING, "名称“" + name + "”应当以小写字母开头", e);
return;
}
} else if (Character.isLowerCase(firstCodePoint)) {
if (initialCaps) {
messager.printMessage(Diagnostic.Kind.WARNING, "名称“" + name + "”应当以大写字母开头", e);
return;
}
} else
conventional = false;
if (conventional) {
int cp = firstCodePoint;
for (int i = Character.charCount(cp); i < name.length(); i += Character.charCount(cp)) {
cp = name.codePointAt(i);
if (Character.isUpperCase(cp)) {
if (previousUpper) {
conventional = false;
break;
}
previousUpper = true;
} else
previousUpper = false;
}
}
if (!conventional)
messager.printMessage(Diagnostic.Kind.WARNING, "名称“" + name + "”应当符合驼式命名法(Camel Case Names)", e);
}
/**
* 大写命名检查,要求第一个字母必须是大写的英文字母,其余部分可以是下划线或大写字母
*/
private void checkAllCaps(Element e) {
String name = e.getSimpleName().toString();
boolean conventional = true;
int firstCodePoint = name.codePointAt(0);
if (!Character.isUpperCase(firstCodePoint))
conventional = false;
else {
boolean previousUnderscore = false;
int cp = firstCodePoint;
for (int i = Character.charCount(cp); i < name.length(); i += Character.charCount(cp)) {
cp = name.codePointAt(i);
if (cp == (int) '_') {
if (previousUnderscore) {
conventional = false;
break;
}
previousUnderscore = true;
} else {
previousUnderscore = false;
if (!Character.isUpperCase(cp) && !Character.isDigit(cp)) {
conventional = false;
break;
}
}
}
}
if (!conventional)
messager.printMessage(Diagnostic.Kind.WARNING, "常量“" + name + "”应当全部以大写字母或下划线命名,并且以字母开头", e);
}
}
}
使用如下代码验证
public class BADLY_NAMED_CODE {
enum colors {
red, blue, green;
}
static final int _FORTY_TWO = 42;
public static int NOT_A_CONSTANT = _FORTY_TWO;
protected void BADLY_NAMED_CODE() {
return;
}
public void NOTcamelCASEmethodNAME() {
return;
}
}
利用上面导入的Javac,参数-processor指定执行编译时需要附带的注解处理器(有多个用逗号分隔)
javac src/com/sun/NameChecker.java
javac src/com/sun/NameCheckProcessor.java
javac -processor com.sun.NameCheckProcessor src/com/sun/BADLY_NAMED_CODE.java
输出如下
