Java开发基础工具的使用(1)——编译与构建

本文试图用通俗的语言从原理和实战上解决Java开发中常常遇到的问题:
为什么同样的代码,在不同的机器上编译效果不一致?
为什么相同的代码,在相同机器的不同IDE中编译效果不一致?
为什么相同的代码,在相同机器上相同IDE中,不同的构建方法的编译结果不一致?

本文并不保证:

  1. 照着做就立马解决现有的编译出错问题—— 因为本文不是排错手册。
  2. 你看完以后,就不会碰到编译出错的问题——光看文章通常解决不了问题,只有在不断磕磕碰碰中实战才能。

JVM 和 类文件

在 Java语言 广受病垢的今天,Java平台却依旧傲然挺立,其中一个重要原因是:Java平台已经通过操作系统中立和语言中立的特性建立了繁荣庞大的生态圈,并因马太效应,大者恒大,让后来者难以追赶。

Java平台的操作系统中立性

每种CPU有自己特有的机器指令,基于CPU之上的每个操作系统有自己特有系统API。如果想实现 一次编写,处处运行 的美梦,就必须将我们的代码和 各种纷繁的CPU、操作系统隔离开来。

Java平台通过 Java虚拟机(JVM)来完成这种隔离,JVM 就像是一个虚拟的操作系统,它接受与真实操作系统无关的指令,将其翻译为对应操作系统的API,完成我们的工作。这种所谓的 与操作系统无关指令 就是 Java字节码(Bytecode)

Bytecode 最初之意是一种用 8 bit 来表示操作码的指令集,所以最多只有256种指令。
其他语言的虚拟机也有自己的 Bytecode,比如Python。
下面的 Bytecode 都特指 Java Bytecode

因为 JVM 需要一种将 Bytecode 翻译成各种操作系统的API,所以针对各个操作系统的 JVM 实现是不一致的,JVM 是一个统称。为了让各种操作系统的 JVM实现 具有最少相同能力,制定了 JVM 规范

工业级的 JVM 当然不仅仅是翻译指令这个功能

现在已有很多 JVM实现。比如最常见的 Hotspot VM,包含在OrancelJDK和OpenJDK之中。它用C++编写,支持主流的操作系统和处理器架构。

As for the whole JDK, HotSpot is supported by Oracle Corporation on Microsoft Windows, Linux, Solaris, and Mac OS X. Supported ISAs are IA-32, x86-64, ARMv6, ARMv7, and SPARC (exclusive to Solaris).

Hotspot为了对程序员屏蔽操作系统和处理器架构的差异,它采取的做法是为支持的操作系统和处理器架构实现了不同的代码(当然大多数是相同的)。

从技术上严格来讲,JVM 并不是直接接受 Bytecode,而是接受由 Bytecode和 其他信息 组成的 类文件(class file)

「其他信息」其实是类文件的重要的的组成部分,比如其中的一项:类文件版本
JVM 能加载主版本号 <= JVM规范对应的类文件,比如 OrancleJDK 7 自带的JVM实现(Hotspot)能运行主版本 <= 51的类文件。

由此可知,你的应用本身是基于JDK8的,但你依赖的三方 lib 可以是 JDK7,JDK6 生成的类文件。

要让所有 JVM实现 都能识别出相同的类文件,必须规定类文件的格式。 JVM规范 专门用了一章来规定类文件的格式,参见 (Chapter 4. The class File Format) 。

JVM 不关心类文件是怎么构建的,也不关心类文件的物理存在方式,它只关心一点:你给我的二进制流要符合JVM类文件格式。

类文件的物理存在方式可以是多种多样的,可以存在远程服务器上,也可以只存在于内存中,最常见的当然是 以 .class 为扩展名的本地磁盘文件。

从技术上来说,类文件可以通过自定义 ClassLoader 从任意地方(本地磁盘,FTPServer,HTTP请求),任意方式(手动构建,调用编译器API现场编译文本文件构建)构建而出。

甚至你可以用一个十六进制编辑器手动编写一个 .class 文件( <–快看神经病)。

Java平台的语言中立性

各种 JVM实现 完成了对下的操作系统中立。

但VM 之上的类文件是晦涩难懂的,并且 JVM 从来不关心类文件 的生成方式,我们可以通过使用对程序员友好的高级语言来生成类文件。
然后通过这些高级语言的编译器加成从 高级语言到 类文件的转换。

喜欢Ruby之自由动态可以用 Groovy
喜欢LISP的人可以用 Clojure
喜欢挑战极限可以用 Scala
喜欢可靠又乖巧的可以用 Kotlin

当然,还有老大哥 Java语言。

虽然JVM设计者很早就许诺保证了JVM的语言中立性,但实际上,JVM规范一直和Java语言紧密相关,直到 JSR-292 发布(引入新操作码invokedynamic),才算是真正的在规范上实现了语言中立

完成各个语言到类文件转换工作的是编译器,编程器可以用任何语言编写的,甚至包括该用语言自身。

Java编译器

Java语言有两个著名的编译器:

  1. Oracle/Sun JDK 自带的作为Java语言规范参考实现的具有官方性质的 javac
  2. Eclipse 在 Eclipse(IDE) 中内置的 ECJ(Eclipse Compiler for Java),也支持独立使用。

这两个编译器都是用Java写的。

上面这句话透露出了两个信息:

  1. 既然是用Java编写的,那么也需要运行在JVM上。
  2. 既然是用Java编写的,那么可以在其他Java程序中直接调用编译器方法。

javac 编译器

javac完全由Java编写,多个操作系统上的 javac 其实是同一份。

在代码中使用 javac

在我们自己的代码中实例化一个 com.sun.tools.javac.main.JavaCompiler 实例并调用其 compiler 方法完成编译工作。

在程序中调用编译器API进行编译的行为,就是动态编译。在线Java编译网站,Tomcat对JSP的支持就利用了动态编译(当然Tomcat使用的不是javac而是ECJ编译器)。

在实际应用中,调用 com.sun.tools.javac.main.JavaCompiler 这种私有API是不好的行为。从 JDK6 开始,通过 JSR199 提供了抽象的编译器API规范:javax.tools.JavaCompiler,javac 和 ECJ 都实现了这个规范。实际应用中,应该通过SPI的方式使用这个规范API。

直接调用 javac 入口Main方法

在程序中用动态编译的时候毕竟较少。

为了在程序之外提供功能,javac通过主类(Main-class) 对外暴露了它的功能,参见:com.sun.tools.javac.Main

这样我们就可以像使用一个普通的Java程序一样使用编译器了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 用调用「普通」Java程序的方式调用 javac(入口方法)
# 下面这种用法 和 在终端中使用 javac/javac.exe 效果一样
>java -cp /Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/lib/tools.jar com.sun.tools.javac.Main

用法: javac <options> <source files>
其中, 可能的选项包括:
-g 生成所有调试信息
-g:none 不生成任何调试信息
-g:{lines,vars,source} 只生成某些调试信息
-nowarn 不生成任何警告
-verbose 输出有关编译器正在执行的操作的消息
-deprecation 输出使用已过时的 API 的源位置
-classpath <路径> 指定查找用户类文件和注释处理程序的位置
-cp <路径> 指定查找用户类文件和注释处理程序的位置
-sourcepath <路径> 指定查找输入源文件的位置
-bootclasspath <路径> 覆盖引导类文件的位置
-extdirs <目录> 覆盖所安装扩展的位置
-endorseddirs <目录> 覆盖签名的标准路径的位置
-proc:{none,only} 控制是否执行注释处理和/或编译。
-processor <class1>[,<class2>,<class3>...] 要运行的注释处理程序的名称; 绕过默认的搜索进程
-processorpath <路径> 指定查找注释处理程序的位置
-parameters 生成元数据以用于方法参数的反射
-d <目录> 指定放置生成的类文件的位置
-s <目录> 指定放置生成的源文件的位置
-h <目录> 指定放置生成的本机标头文件的位置
-implicit:{none,class} 指定是否为隐式引用文件生成类文件
-encoding <编码> 指定源文件使用的字符编码
-source <发行版> 提供与指定发行版的源兼容性
-target <发行版> 生成特定 VM 版本的类文件
-profile <配置文件> 请确保使用的 API 在指定的配置文件中可用
-version 版本信息
-help 输出标准选项的提要
-A关键字[=值] 传递给注释处理程序的选项
-X 输出非标准选项的提要
-J<标记> 直接将 <标记> 传递给运行时系统
-Werror 出现警告时终止编译
@<文件名> 从文件读取选项和文件名

执行结果如上所示,和执行 javac 命令的执行结果一模一样。

javac 命令 或 javac.exe 只是对上面的入口方法的封装而已,传递给 javac/javac.exe 的参数最终都会转发给 com.sun.tools.javac.Main 的入口方法。

随便说一下,JDK提供的 jps,jconsole,jinfo,jstack,jstat 等命令和 javac 类似,他们都是Launcher,实际都是用Java写成,代码在 tools.jar 中。参见:tools

我们也可以使用 shell 写自己的Launcher,只需要调用 java -cp xx MainClass 并转发参数即可。

使用笨拙的方式编译 Java8App.java :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Java8App {
public static void main(String[] args) {

User user=new User();
user.java8();
}

/**
* 使用了Java8新增的方法引用:System.out::println
*/

public static class User {
public void java8(){

IntStream list= IntStream.range(1,10);

list.forEach(System.out::println);

System.out.println("DONE");
}
}

编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>ll
total 32
-rw-r--r-- 1 ljh staff 208 5 27 07:49 Java5App.java
-rw-r--r-- 1 ljh staff 716 5 27 08:04 Java7App.java
-rw-r--r-- 1 ljh staff 509 5 26 11:57 Java8App.java
-rw-r--r-- 1 ljh staff 291 5 27 07:49 User.java

#编译Java8App.java
>java -cp /Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/lib/tools.jar com.sun.tools.javac.Main Java8App.java

>ll
-rw-r--r-- 1 ljh staff 208 5 27 07:49 Java5App.java
-rw-r--r-- 1 ljh staff 716 5 27 08:04 Java7App.java
-rw-r--r-- 1 ljh staff 1258 5 28 09:55 Java8App$User.class
-rw-r--r-- 1 ljh staff 447 5 28 09:55 Java8App.class
-rw-r--r-- 1 ljh staff 509 5 26 11:57 Java8App.java
-rw-r--r-- 1 ljh staff 291 5 27 07:49 User.java

注意分清「JVM参数」和「程序参数」。
在「java -cp /Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/lib/tools.jar com.sun.tools.javac.Main Java8App.java」中:
主类「com.sun.tools.javac.Main」
之前的是「JVM参数」,比如 -cp /Lixx;
主类之后的是「程序参数」,比如 Java8App.java 。

看结果中的 Java8App.class 和 Java8App$User.class 知道编译成功。

1
2
3
4
#使用 javap 命令反编译 Java8App.class 文件,探测类文件版本号。
>javap -verbose Java8App|grep version
minor version: 0
major version: 52

ECJ 编译器

再来说说 ECJ。
ECJ最开始是内置于Eclipse的,后来提供了 standalone 版。喜闻乐见,这个所谓的standalone 版本其实就是 jar 包而已。

ECJ并没有提供本地Lanucher。

和 javac 一样,在程序中可以通过 JSR199标准API 或者 ECJ的私有PAI来使用ECJ,(略)。

前文已述,Tomcat就是通过ECJ实现JSP的动态编译的:首先,Tomcat使用它的JSP解析器将JSP文件转换成类,然后用ECJ编译这些类。

安装 ECJ 并查看 main 入口方法:

1
2
#其实仅仅下载了一个 etc.jar 包而已,完全可以从 Eclipse 中 copy。
brew install ecj

1
2
#使用java这个Launcher调用ECJ本体
java -cp /usr/local/Cellar/ecj/4.9/share/java/ecj.jar org.eclipse.jdt.internal.compiler.batch.Main

因为我们没有传参数,所以输出结果是 ECJ 的help提示,版本为3.9.0。

Eclipse Compiler for Java(TM) bundle_qualifier, 3.9.0
Copyright IBM Corp 2000, 2013. All rights reserved.

 Usage:  
 If directories are specified, then their source contents are compiled.
 Possible options are listed below. Options enabled by default are prefixed
 with '+'.

 Classpath options:
    -cp -classpath 
                       specify location for application classes and sources.
                       Each directory or file can specify access rules for
                       types between '[' and ']' (e.g. [-X] to forbid
                       access to type X, [~X] to discourage access to type X,
                       [+p/X:-p/*] to forbid access to all types in package p
                       but allow access to p/X)
    -bootclasspath 
                       specify location for system classes. Each directory or
                       file can specify access rules for types between '['
                       and ']'
    -sourcepath 
                       specify location for application sources. Each directory
                       or file can specify access rules for types between '['
                       and ']'. Each directory can further specify a specific
                       destination directory using a '-d' option between '['
                       and ']'; this overrides the general '-d' option.
                       .class files created from source files contained in a
                       jar file are put in the user.dir folder in case no
                       general '-d' option is specified. ZIP archives cannot
                       override the general '-d' option
    -extdirs 
                       specify location for extension ZIP archives
    -endorseddirs 
                       specify location for endorsed ZIP archives
    -d            destination directory (if omitted, no directory is
                       created); this option can be overridden per source
                       directory
    -d none            generate no .class files
    -encoding     specify default encoding for all source files. Each
                       file/directory can override it when suffixed with
                       '['']' (e.g. X.java[utf8]).
                       If multiple default encodings are specified, the last
                       one will be used.

 Compliance options:
    -1.3               use 1.3 compliance (-source 1.3 -target 1.1)
    -1.4             + use 1.4 compliance (-source 1.3 -target 1.2)
    -1.5 -5 -5.0       use 1.5 compliance (-source 1.5 -target 1.5)
    -1.6 -6 -6.0       use 1.6 compliance (-source 1.6 -target 1.6)
    -1.7 -7 -7.0       use 1.7 compliance (-source 1.7 -target 1.7)
    -source   set source level: 1.3 to 1.7 (or 5, 5.0, etc)
    -target   set classfile target: 1.1 to 1.7 (or 5, 5.0, etc)
                       cldc1.1 can also be used to generate the StackMap
                       attribute

 Warning options:
    -deprecation     + deprecation outside deprecated code (equivalent to
                       -warn:+deprecation)
    -nowarn -warn:none disable all warnings
    -nowarn:[]
                       specify directories from which optional problems should
                       be ignored
    -?:warn -help:warn display advanced warning options

 Error options:
    -err:    convert exactly the listed warnings
                                      to be reported as errors
    -err:+   enable additional warnings to be
                                      reported as errors
    -err:-   disable specific warnings to be
                                      reported as errors

 Setting warning or error options using properties file:
    -properties    set warnings/errors option based on the properties
                          file contents. This option can be used with -nowarn,
                          -err:.. or -warn:.. options, but the last one on the
                          command line sets the options to be used.

 Debug options:
    -g[:lines,vars,source] custom debug info
    -g:lines,source  + both lines table and source debug info
    -g                 all debug info
    -g:none            no debug info
    -preserveAllLocals preserve unused local vars for debug purpose

 Annotation processing options:
   These options are meaningful only in a 1.6 environment.
    -Akey[=value]        options that are passed to annotation processors
    -processorpath 
                         specify locations where to find annotation processors.
                         If this option is not used, the classpath will be
                         searched for processors
    -processor 
                         qualified names of the annotation processors to run.
                         This bypasses the default annotation discovery process
    -proc:only           run annotation processors, but do not compile
    -proc:none           perform compilation but do not run annotation
                         processors
    -s              destination directory for generated source files
    -XprintProcessorInfo print information about which annotations and elements
                         a processor is asked to process
    -XprintRounds        print information about annotation processing rounds
    -classNames 
                         qualified names of binary classes to process

 Advanced options:
    @            read command line arguments from file
    -maxProblems    max number of problems per compilation unit (100 by
                       default)
    -log         log to a file. If the file extension is '.xml', then
                       the log will be a xml file.
    -proceedOnError[:Fatal]
                       do not stop at first error, dumping class files with
                       problem methods
                       With ":Fatal", all optional errors are treated as fatal
    -verbose           enable verbose output
    -referenceInfo     compute reference info
    -progress          show progress (only in -log mode)
    -time              display speed information 
    -noExit            do not call System.exit(n) at end of compilation (n==0
                       if no error)
    -repeat         repeat compilation process  times for perf analysis
    -inlineJSR         inline JSR bytecode (implicit if target >= 1.5)
    -enableJavadoc     consider references in javadoc
    -Xemacs            used to enable emacs-style output in the console.
                       It does not affect the xml log output
    -missingNullDefault  report missing default nullness annotation

    -? -help           print this help message
    -v -version        print compiler version
    -showversion       print compiler version and continue

 Ignored options:
    -J
先删除刚才用javac生成的 .class 文件,然后用ECJ编译 Java8App.javaA:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#编译Java8App.java
>java -cp /usr/local/Cellar/ecj/4.9/share/java/ecj.jar org.eclipse.jdt.internal.compiler.batch.Main Java8App.java

----------
----------
1. ERROR in /Users/ljh/work/code/learningshowcase/classversion/src/main/java/com/liufor/learningshowcase/classversion/Java8App.java (at line 23)
list.forEach(System.out::println);
^^^^^^^^^^^^^^^^^^^^
Syntax error on token(s), misplaced construct(s)
----------
2. ERROR in /Users/ljh/work/code/learningshowcase/classversion/src/main/java/com/liufor/learningshowcase/classversion/Java8App.java (at line 23)
list.forEach(System.out::println);
^
Syntax error on token ":", invalid (
----------
3. ERROR in /Users/ljh/work/code/learningshowcase/classversion/src/main/java/com/liufor/learningshowcase/classversion/Java8App.java (at line 23)
list.forEach(System.out::println);
^
Syntax error, insert "AssignmentOperator Expression" to complete Expression
----------
3 problems (3 errors)%
悲剧!编译错误,提示为语法错误,不识别方法引用 System.out::println ,明显是ECJ的语言级别低于1.8. 我们倒回去,看看第一个运行ecj时的help 输出:
Compliance options:
    -1.3               use 1.3 compliance (-source 1.3 -target 1.1)
    -1.4             + use 1.4 compliance (-source 1.3 -target 1.2)
    -1.5 -5 -5.0       use 1.5 compliance (-source 1.5 -target 1.5)
    -1.6 -6 -6.0       use 1.6 compliance (-source 1.6 -target 1.6)
    -1.7 -7 -7.0       use 1.7 compliance (-source 1.7 -target 1.7)
    -source   set source level: 1.3 to 1.7 (or 5, 5.0, etc)
    -target   set classfile target: 1.1 to 1.7 (or 5, 5.0, etc)
                       cldc1.1 can also be used to generate the

看来,我的ECJ最高支持支持到Java7,并且注意到 use 1.4 compliance 前有个 + 号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#作为 DEMO 的 Java5App.java

public class Java5App {
public static void main(String[] args) {

User user = new User();
user.java5();
}

/**
* 使用了Java5新增的泛型集合List<E>
*/
public static class User {
public void java5() {
List<String> strings = new ArrayList<String>();
System.out.println("Java5");
}
}
}

用ECJ编译 Java5App.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>java -cp /usr/local/Cellar/ecj/4.9/share/java/ecj.jar org.eclipse.jdt.internal.compiler.batch.Main Java5App.java

----------
1. ERROR in /Users/ljh/work/code/learningshowcase/classversion/src/main/java/com/liufor/learningshowcase/classversion/Java5App.java (at line 23)
List<String> strings = new ArrayList<String>();
^^^^^^
Syntax error, parameterized types are only available if source level is 1.5 or greater
----------
2. ERROR in /Users/ljh/work/code/learningshowcase/classversion/src/main/java/com/liufor/learningshowcase/classversion/Java5App.java (at line 23)
List<String> strings = new ArrayList<String>();
^^^^^^
Syntax error, parameterized types are only available if source level is 1.5 or greater
----------
2 problems (2 errors)%

额,依旧出错。不识别 Java5 新增的泛型,并且提示将 source level 设置为 >= 1.5。

那个 + 号表示默认 -source 1.3 -target 1.2

手动设置 -source 为1.5后,再次编译,成功!

1
2
3
4
5
6
7
8
>java -cp /usr/local/Cellar/ecj/4.9/share/java/ecj.jar org.eclipse.jdt.internal.compiler.batch.Main Java5App.java -source 1.5

>ll
-rw-r--r-- 1 ljh staff 798 5 28 11:06 Java5App$User.class
-rw-r--r-- 1 ljh staff 443 5 28 11:06 Java5App.class
-rw-r--r-- 1 ljh staff 597 5 28 11:06 Java5App.java
-rw-r--r-- 1 ljh staff 590 5 28 10:22 Java7App.java
-rw-r--r-- 1 ljh staff 538 5 28 10:25 Java8App.java

编译时 -source 、 -target

javac 和 ECJ 都支持设置编译级别的参数: -source、-target。
前者表示编译器把源代码当作哪个语言版本。后者表示生成的类文件对应的JVM规范版本。

因为这两个参数的提供,编译器保证了兼容性。

javac 实现的 Java语言规范(JLS) 和JVM规范以及对应的类文件版本:
|javac版本|JDK6 javac 各个版本|JDK7 javac 各个版本|JDK8 javac 各个版本|
| ———- | — |—-|——-|
| 实现的 JLS 版本 |1.6|1.7|1.8|
| 实现的 JVM规范版本 |1.6|1.7|1.8|
| 能支持的最高类文件版本 |50.0|51.0|52.0|

-source 取值

javac

javac编译器的默认 -source 取值为该javac支持的最高版本。比如:

JDK8 javac默认 -source 1.8.
JDK 7 javac 默认 -source 1.7

1
2
3
4
5
6
7
8
>javac -version
javac 1.8.0_51

>javac Java5App.java

>javap -v Java5App |grep version
minor version: 0
major version: 52

也可以特别指定(废话)。

ECJ

ECJ编译器的默认 -source 取值并不是它能支持的最高版本,比如 3.9.0 这个版本的ECJ最高支持Java7,然后默认情况下,连 Java5 都不支持,参见上文。

所以强烈建议:在构建配置文件中 显式指定编译器的 -source 参数,否则换编译器后,可能导致构建失败。

-target 取值

javac

javac 的取值逻辑见文档:Oracel JDK 8 javacOracel JDK 7 javac

如果 -source 未指定,则 -target 为最高支持版本。
如果已指定 -source,则 -target 取值依赖于(但并不等于) -source。

JDK8 javac, -source取值为[1.5, 1.6, 1.7, 1.8]时,-target 都是1.8
JDK7 javac, -source取值为[1.5, 1.6, 1.7]时,-target 都是1.7
JDK6 javac, -source取值为[1.5, 1.6]时,-target 都是1.6

也就是说 javac 允许 -target 比 -source 高,并且在只指定-source参数的时候,还是默认应用这个规则的。
这种规则极有可能误导使用者:使用者为了兼容而显式指定低版本 -source ,结果生成的字节码确是只能在高版本VM上运行,结果就会悲剧的 UnsupportedClassVersionError

下面演示这种悲剧的 的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
>javac -version
javac 1.8.0_51

#本意是生成兼容JDK7的代码,虽然有警告,但是编译成功
>javac Java7App.java -source 1.7
警告: [options] 未与 -source 1.7 一起设置引导类路径
1 个警告

#运行失败,发生臭名昭著的 UnsupportedClassVersionError
>$JAVA7_HOME/bin/java com.liufor.learningshowcase.classversion.Java7App

Exception in thread "main" java.lang.UnsupportedClassVersionError: com/liufor/learningshowcase/classversion/Java7App : Unsupported major.minor version 52.0
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:800)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:449)
at java.net.URLClassLoader.access$100(URLClassLoader.java:71)
at java.net.URLClassLoader$1.run(URLClassLoader.java:361)
at java.net.URLClassLoader$1.run(URLClassLoader.java:355)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:354)
at java.lang.ClassLoader.loadClass(ClassLoader.java:425)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
at java.lang.ClassLoader.loadClass(ClassLoader.java:358)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:482)

javac不允许 -source 比 -target 高:

1
2
>javac Java5App.java -source 1.7 -target 1.6
javac: 源发行版 1.7 需要目标发行版 1.7

如果配置的版本超过了 javac 的能支持的最高版本,出现以下悲剧:
无效的源发行版无效的目标发行版

1
2
3
4
5
6
7
8
9
10
11
12
13
>javac -version
javac 1.7.0_79

>javac Java8App.java -source 1.8 -target 1.8
javac: 无效的源发行版: 1.8
用法: javac <options> <source files>
-help 用于列出可能的选项

>javac Java8App.java -target 1.8
javac: 无效的目标发行版: 1.8
用法: javac <options> <source files>
-help 用于列出可能的选项
>

ECJ

ECJ 的 -target 取值相比 javac 设计的更好,它不允许 -target 比 -source 高,这部分减少了 UnsupportedClassVersionError

综上所述,保持良好习惯,不但显式指定 -source ,也显样式指定 -target

后情提要,目前常用版本的 maven 会将编译器默认设置 -source 为1.5,-target 为1.5 。这可能导致用 IDE 能编译的项目,用 maven 编译不过。

在实际运维和开发中,一般不是在终端手动调用编译器,而是在 构建工具(比如Maven) 或者 IDE 中调用。

在其他地方调用编译器

in Maven & in Maven with IntelliJ IDEA

maven基础

maven 是使用率依旧很高的构建工具,虽然它可能不如 gradle 那么性感。

声明:以下关于Maven的内容都只针对 maven 3,新项目绝对不要基于 maven 2 构建。

maven 本身不做任何事情,它的实际功能能都由插件(maven plugin)完成。
maven 将整个 build 过程分为多个 lifecycle。
每个 maven plugin 可以绑定到多个 lifecycle 上,每个 lifecycle 上 可以被绑定多个 maven plugin,也就说说 maven lifecycle 和 maven plugin 是 N : M 关系。

maven 读取 setting.xml(全局和用户级别的)、pom.xml 以及其他方式的 配置信息完成项目的构建。

maven本质

maven和javac一样,本质上是 Java 写成的程序。并通过了一个 shell/bat 写成的Launcher:mvn,实际调用java执行入口方法。

比如,在我的一个项目里

1
2
3
4
5
6
>echo $JAVA_HOME
/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home

>mvn #mvn命令在classverson项目里,其实等于下面一大坨代码

>/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/bin/java -classpath /usr/local/Cellar/maven/3.3.3/libexec/boot/plexus-classworlds-2.5.2.jar -Dclassworlds.conf=/usr/local/Cellar/maven/3.3.3/libexec/bin/m2.conf -Dmaven.home=/usr/local/Cellar/maven/3.3.3/libexec -Dmaven.multiModuleProjectDirectory=/Users/ljh/work/code/learningshowcase/classversion org.codehaus.plexus.classworlds.launcher.Launcher

我使用了哪个maven版本?

终端
1
2
3
4
5
6
7
>mvn -v
Apache Maven 3.3.3 (7994120775791599e205a5524ec3e0dfe41d4a06; 2015-04-22T19:57:37+08:00)
Maven home: /usr/local/Cellar/maven/3.3.3/libexec
Java version: 1.8.0_51, vendor: Oracle Corporation
Java home: /Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre
Default locale: zh_CN, platform encoding: UTF-8
OS name: "mac os x", version: "10.11.4", arch: "x86_64", family: "mac"
IntelliJ IDEA

Preferences->Build,Execution,Deployment->Build Tools->Maven->Runner->Maven home directory
image_1aju45bu3i613oiue01l5417j3m.png-93.7kB

IDEA 内置maven3和maven2,但是强烈建议不要使用它们,最好和终端环境保持一致。

因为终端配置更有可能和线上部署系统的配置是一致的。所以保持IDE和终端配置一致,就是保持开发环境和线上配置一致。这是良好的编程习惯,能减少一些不必要的麻烦。

在终端和 IDEA 中使用不同的Maven版本,可能会给你带来麻烦。

用哪个JRE启动的Maven?

如上面所示,mvn 从操作系统环境变量 JAVA_HOME 推断出 JRE(JDK8),并用它作为 Maven 的启动器。

在IntelliJ IDEA中,可以为 maven 指定 JRE:
Preferences->Build,Execution,Deployment->Build Tools->Maven->Runner

IDEA中Maven runner的配置

请保持 IDE maven的启动JRE 和 终端mvn 的JRE一致。

为了更利于使用,IDE 可能会修改maven的一些默认配置,所以 mvn 和 IDE中的Maven 可能表现不一致。

Maven的启动JRE很重要,因为它关系到编译行为。

Maven中的编译器

maven 通过 maven-compiler-plugin 插件绑定到 complier lifecycle 实现编译功能。

你可能料到了maven-compiler-plugin 是支持多种编译器的,它甚至支持 C# 编译器(What !~)。

maven-compiler-plugin 通过两个配置项确定使用哪个编译器:

  1. complierId
  2. 编译器对应的 dependency

maven-compier-plugin 的默认配置参考: AbstractCompilerMojo

其中几个值如下:

  1. compilerId: javac ,指定使用 javac 编译器。
  2. 编译器版本:编译器版本 和 Maven的启动JRE对应的JDK的相对应。
  3. -source:1.5。
  4. -target:1.5。
  5. debug: 默认true。编译生成类文件时,是否包含调试信息(比如方法参数名称,局部变量名称,行号以及源文件地址等),相当于 jvac 和 ECJ 的 -g 参数。
启用javac

因为compilerId默认为javac,并且 maven-compiler-plugin 默认包含 javac的 dependency,所以并不需要特别配置。

javac的依赖是:org.codehaus.plexus:plexus-compiler-javac

1
2
3
4
5
6
7
8
9
10
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<!--默认就为javac,可配可不配-->
<compilerId>javac</compilerId>
<target>1.8</target>
<source>1.8</source>
</configuration>
<version>3.5.1</version>
</plugin>

通过上面的配置,我们知道启用了javac,但是我们还不知道是通过怎样的方式调用的?javc是哪个版本的?

javac的调用逻辑和版本确定是在 org.codehaus.plexus:plexus-compiler-javac 中确定的。

javac的fork模式

plugin 有一个 fork 配置项,是javac特有的功能,ECJ没有。它决定编译流程是在Maven实例的进程中进行,还是单独开启一个进程进行。

非fork模式时,如果Maven实例的环境支持JSR199,则通过 javax.tools.ToolProvider#getSystemJavaCompiler获取Maven实例的 java.home 系统属性 指向的编译器进行编译。说白了,就是当Maven用JDK8启动,则使用JDK8自带的javac,如果用JDK7启动,则使用JDK7自带的javac。

fork模式指定单独的进程运行javac,所以需要另外一个参数 executable 指定javac的路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<!--默认就为javac,可配可不配-->
<compilerId>javac</compilerId>
<target>1.8</target>
<source>1.8</source>

<!--在新进程中启动编译器-->
<fork>true</fork>
<executable>${JAVA8_HOME}/bin/javac</executable>

</configuration>
<version>3.5.1</version>
</plugin>

所以javac版本决定方式为:如果是fork模式,则由配置的executable决定。如果是普通模式,则和 Maven 的启动器一致。

启用ECJ

maven-compiler-plugin 并没有包含 ECJ 的 dependency,开启 ECJ 编译器需要设置 compilerId 并添加依赖如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<!--开启ECJ编译器条件1: compilerId-->
<compilerId>eclipse</compilerId>
<target>1.8</target>
<source>1.8</source>
</configuration>
<version>3.5.1</version>
<!--开启ECJ编译器条件2: compilerId-->
<dependencies>
<dependency>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-compiler-eclipse</artifactId>
<version>2.7</version>
</dependency>
</dependencies>
</plugin

ECJ 的版本选择是由 org.codehaus.plexus:plexus-compiler-eclipse 决定的,它的决定方式比较简单:它直接依赖于 org.eclipse.tycho:org.eclipse.jdt.core,所以这个依赖的jdt是什么,那么ECJ就是那个版本的。所以,你可以exclusion掉配好的ECJ版本,让 maven-compiler-plugin直接依赖另外一个版本——好神经病的做法,你大概一辈子都不会用到。

名不副实的 compilerVersion 配置项

从上面的分析,我们已经知道了,编译器和编译器版本的选择略微复杂,并不是由 plugin 的 compilerVersion 配置项决定的。

这个配置项的名字很有迷惑性,它的作用比较特殊,这里不讲述了。

设置-source

由 source 配置项确定,对javac和ECJ都生效。

设置-target

由 target 配置项确定,对javac和ECJ都生肖。

给编译器传递其他参数

maven-compiler-plugin 抽象了通用的一些编译器参数,通过 节点配置,比如通用的 source,target 参数。如果需要指定未抽象出的参数或者编译器特殊的参数,则通过 compilerArgs 传递。

下面这个例子通过特殊参数解决单独进程编译器的乱码问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerId>javac</compilerId>

<!--对javac编译器来说,target不能比source小,这里特意配置错误以使起输出中文ERROR-->
<target>1.7</target>
<source>1.8</source>
<encoding>utf-8</encoding>

<compilerArgs>
<!--在 IntelliJ 中通过 Maven 构建时,这条必须加上,否则日志中的中文会乱码-->
<!--通过命令行mvn构建时, 可加可不加-->
<!--注意这里有个 -J 前缀-->
<arg>-J-Dfile.encoding=UTF-8</arg>
</compilerArgs>

<!--使用不同于Maven的新进程启动编译,新进程里打印出的中文错误消息是乱码的-->
<fork>true</fork>
<compilerVersion>1.8</compilerVersion>
<executable>${JAVA8_HOME}/bin/javac</executable>

</configuration>
<version>3.5.1</version>

</plugin>

程序员不喜欢黑匣子,从前面分析 maven 编译代码 的本质时,我们知道 maven 中配置的参数最终是被转发给了具体编译器的。比如上面一个例子中,执行的编译代码究竟是怎样生成的?

通过 -X 开启 mvn 详细日志查看究竟:

1
2
3
4
5
6
7
8
9
>mvn install -X 

---省略----
DEBUG] Excutable:
[DEBUG] /Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/bin/javac
[DEBUG] Command line options:
[DEBUG] -d /Users/ljh/work/code/learningshowcase/classversion/target/classes -classpath /Users/ljh/work/code/learningshowcase/classversion/target/classes: -sourcepath /Users/ljh/work/code/learningshowcase/classversion/src/main/java:/Users/ljh/work/code/learningshowcase/classversion/target/generated-sources/annotations: /Users/ljh/work/code/learningshowcase/classversion/src/main/java/com/liufor/learningshowcase/classversion/Java8App.java /Users/ljh/work/code/learningshowcase/classversion/src/main/java/com/liufor/learningshowcase/classversion/Java5App.java /Users/ljh/work/code/learningshowcase/classversion/src/main/java/com/liufor/learningshowcase/classversion/Java7App.java -s /Users/ljh/work/code/learningshowcase/classversion/target/generated-sources/annotations -g -nowarn -target 1.7 -source 1.8 -encoding utf-8

---省略----

通过 mvn 的 -X 参数,开启maven DEBUG 信息后,很容易知道最终执行编译的是 javac 的路径,以及maven转发给它的参数。

在 IntelliJ 中,通过 UI Output level: DEBUG 开启 -X 相同的功能:
image_1ajs6hhht4p07mb27b7rj1tutm.png-62.7kB

in IntelliJ IDEA

使用 IDEA 本身的功能,也能完成编译/构建工作。

IDEA中支持几种构建,参见:编译类型。都在 Build 菜单下。
Compiler 「scope」 :编译选中的目标源文件,可能是单个文件,也可能是多个文件,也可能是一个包。
Make Module YyyModuleMake Project:编译整个 Module和它的依赖Module 中的修改过的源文件。 编译整个 Poject项目中的被修改过的源文件。
Rebuild Project:完全重新编译整个项目。

选择编译器

Preferences->Build,Execution,Deployment->Compiler->Java Compiler->Use compiler

image_1ajuk11ia13tvj91pl916dk1i9r13.png-68.7kB
UI见上图,可以选择javac,ECJ(Eclipse),aspecjt等。

编译器用的哪个版本?

当使用javac时,由Module的 ModuleSDK 决定使用那个版本的javac。
image_1ajuna4slrvn1u3c1at215j116aj1g.png-73.2kB

当使用ECJ时,使用IDEA内部带的版本(除非替换安装目录下的Contents/lib/ecj-x.y.z.jar,否则不能换版本)

设置-source

项目结构中对应Module的Language-level
image_1ajunat2bqi8t9j1cclb241jgi1t.png-109.5kB

设置-target

Preferences->Build,Execution,Deployment->Compiler->Java Compiler->Per-module bytecode version:

image_1ajuk11ia13tvj91pl916dk1i9r13.png-68.7kB

UI见上图,分别设置每个module的字节码版本,也可以使用 Project bytecode version设置所有项目的字节码版本。

给编译器传递其他参数

Preferences->Build,Execution,Deployment->Compiler->Java Compiler->Additional command line parameters:

上图中的 Javac Options 表示传递给 javac的参数,在UI上可以配置常见的3个,其他的通过 command line parameters 配置。

pom.xml可能影响IDEA的配置

IDEA 通过 IDEA plugin(Maven Integration)实现了对 Maven 的集成,Maven Integration 有一个隐藏的功能,可能会引起使用者的不适:
它会自动使 IDEA 的 Language levelTarget bytecode version 和 pom.xml 保持一致,如果当你在IDEA中修改者两者后,却被莫名奇妙还原的话,你得修改 pom.xml 。

调用编译器API

略。

热评文章