起因
 
- 不知道大家在开发中,有没有过下面这些疑问。有的话,今天就一次解答清楚。
 
 
- 如何使用javac命令编译一个项目?
 - java或者javac的一些参数到底有什么用?
 - idea或者maven是如何编译java项目的?(你可能猜测底层是javac,但是你没有证据)。
 - 自己能不能写一个程序,去编译一个项目。
 
 
Javac命令详解
 
简单使用
 
 
public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}
 
- java Hello
 - 上面的命令是通过javac命令编译一个java文件,它会生成一个Hello.class文件。然后使用java命令运行Hello程序。
 
 
带包名编译运行
 
 
package com.maple;
public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}
 
- 然后使用
java com.maple.Hello运行。但是你会发现报错了。 
 
java Hello
错误: 找不到或无法加载主类 Hello
java com.maple.Hello
错误: 找不到或无法加载主类 com.maple.Hello
 
- 需要将编译之后的Hello.class文件移动到目录
com/maple中去,并在com的同级目录执行java com.maple.Hello即可正常执行。 - – com 
   
 - 在com的上级目录执行。java命令会解析待执行的全类名,并在目标目录中选择class文件。
 
 
误区纠正
 
- javac java文件路径.java
 - java 需要执行的带有main方法的全限定类名 
  
- 带包名的需要在顶级包目录执行。
 - 使用全类名(带包路径 例如:com.maple.Hello)
 
  - 运行的类文件从classpath中找。没有指定默认当前目录。 
  
- 例如我现在在
D:\test目录下有com\maple\Hello.那么我可以在test目录下正常执行java com.maple.Hello. - 同时也可以在任意目录执行
java -cp D:\test com.maple.Hello.指定classpath执行。 - 也可以先设置classpath 
$env:classpath="$env:classhatp;d:\test",然后再任意目录执行java com.maple.Hello 
  
 
全部参数
 
- 参考文档:https://docs.oracle.com/en/java/javase/11/tools/javac.html#GUID-AEEC9F07-CB49-4E96-8BC7-BCC2C7F725C9
 
 
用法: 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 -d target/ -cp lib/ -sourcepath lib1 Test.java
 - javac -d 【class文件生成目录】 -cp 【使用到的class依赖位置】-sourcepath 【依赖的其他类源路径】【待编译的java文件】
 
 
自制编译器
 
- 实现一个简单的编译器,来编译自己的项目。
 - 编译项目,文件都是多个呢,那么javac要如何编译多个java文件呢。第一种方式,可以直接在javac后跟上多个java文件路径,就能编译多个。或者使用@文件名,在文件中指定要编译的java文件。
 - 然后还有一个就是依赖问题,如果项目引用了其他依赖,那么就需要在编译时通过-cp将依赖库添加的classpath中。
 - 在一个就是文件编码,我们使用中文,需要使用-encoding指定utf-8.
 - 通过-d参数指定class文件输出目录。
 - 最终效果如下
 
 

 
- 同时如果有外部依赖,可以使用-cp指定依赖库地址。
 - 简单代码如下:
 
 
package com.maple.compiler;
import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class MyJavaCompilerTool {
    private final Path projectPath;
    private final Path outputPath;
    private final List<String> additionalJavacArgs;
    public MyJavaCompilerTool(String projectPath, String outputPath, List<String> additionalJavacArgs) {
        this.projectPath = Paths.get(projectPath).toAbsolutePath();
        this.outputPath = outputPath != null ? Paths.get(outputPath).toAbsolutePath() :
        this.projectPath.resolve("output");
        this.additionalJavacArgs = additionalJavacArgs != null ? additionalJavacArgs : new ArrayList<>();
    }
    public void compile() throws IOException, InterruptedException {
        
        String canCompile = isProjectPath(projectPath);
        if (!canCompile.isEmpty()) {
            System.err.println(canCompile);
            return;
        }
        
        System.out.println("\n===========================【创建输出目录】=============================");
        System.out.println(outputPath);
        Files.createDirectories(outputPath);
        
        System.out.println("\n===========================【获取待编译文件】=============================");
        List<Path> javaFiles = findJavaFiles(projectPath);
        System.out.println(javaFiles);
        if (javaFiles.isEmpty()) {
            System.out.println("No Java files found in the project directory.");
            return;
        }
        
        List<String> command = new ArrayList<>();
        command.add("javac");
        command.add("-encoding");
        command.add("utf-8");
        command.addAll(additionalJavacArgs);
        command.add("-d");
        command.add(outputPath.toString());
        command.addAll(javaFiles.stream().map(Path::toString).collect(Collectors.toList()));
        
        String commandString = command.stream()
                .map(arg -> arg.contains(" ") ? "\"" + arg + "\"" : arg)
                .collect(Collectors.joining(" "));
        System.out.println("\n===========================【开始编译】=============================");
        System.out.println("Executing command: " + commandString);
        
        ProcessBuilder processBuilder = new ProcessBuilder(command);
        
        processBuilder.redirectErrorStream(true);
        Process process = processBuilder.start();
        
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        }
        int exitCode = process.waitFor();
        if (exitCode == 0) {
            System.out.println("\n===========================【编译成功】=============================");
            System.out.println("Compilation successful. Output directory: " + outputPath);
        } else {
            System.err.println("\n===========================【编译失败】=============================");
            System.err.println("Compilation failed with exit code: " + exitCode);
        }
    }
    private List<Path> findJavaFiles(Path directory) throws IOException {
        List<Path> javaFiles = new ArrayList<>();
        Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
                if (file.toString().endsWith(".java")) {
                    javaFiles.add(file);
                }
                return FileVisitResult.CONTINUE;
            }
        });
        return javaFiles;
    }
    private static String isProjectPath(Path directory) {
        
        File file = directory.toFile();
        File[] subFileList = file.listFiles();
        List<String> supportFileList = Arrays.asList("java", "com", "main", "src");
        String canCompile = "项目路径【" + directory + "】不支持编译 项目路径下请包含下列其中一种文件夹: " + String.join(" ", supportFileList);
        if (subFileList != null) {
            for (File subFile : subFileList) {
                if (subFile.isDirectory() && supportFileList.contains(subFile.getName())) {
                    canCompile = "";
                    break;
                }
            }
        }
        return canCompile;
    }
    public static void main(String[] args) {
        String projectPath;
        if (args.length < 1) {
            System.out.println("Usage: java MyJavaCompilerTool [projectPath:缺省时为当前目录] [javac args...]");
            projectPath = System.getProperty("user.dir");
        } else {
            projectPath = args[0];
        }
        if (projectPath.startsWith("C:")) {
            System.out.println("C盘路径,不支持编译");
            return;
        }
        List<String> javacArgs = args.length > 1 ? Arrays.asList(args).subList(1, args.length) : new ArrayList<>();
        try {
            System.out.println("\n===========================【开始执行】=============================");
            MyJavaCompilerTool compiler = new MyJavaCompilerTool(projectPath, null, javacArgs);
            compiler.compile();
        } catch (IOException | InterruptedException e) {
            System.err.println(e.getMessage() + " detail: " + e);
        }
    }
}
 
Maven编译
 
生命周期与插件
 
- maven定义了一系列的生命周期,这些生命周期我们可以理解为是抽象的接口,实现具体功能是通过插件来实现的。对应的maven插件实现了某个生命周期的能力。
 - 例如maven中的
compile生命周期,它对应的默认实现就是maven-compiler-plugin这个插件。 
 
Maven如何编译java项目
 
- 参考:https://maven.apache.org/developers/mojo-api-specification.html
 - 插件由一个或多个 Mojo 组成,每个 Mojo 都是插件目标之一的实现。Mojo 必须有一个名为 execute 的方法,该方法不声明任何参数,并且具有 void 返回类型。
 - 简单理解插件的入口就是实现了
Mojo类的execute方法。例如编译插件的入口就是CompilerMojo.exeute() 
 

 
Debugger(调试) Maven插件
 
方式1 (下载源代码项目)
 
- 首先确保maven使用的插件和你现在的源代码项目版本一致 
  
- 首先查看maven插件的版本
 
- 去github下载对应版本的插件(https://github.com/apache/maven-compiler-plugin/tags)
 
  - 进入一个普通项目,作为maven调试的入口。在pom同级目录下执行
mvnDebug compile,然后maven会输出一个端口,复制这个端口号。 
  
  - 打开
maven-compiler-plugin源代码项目,添加一个远程jvm调试,端口号使用刚刚的端口号。 
 

 
- 在
CompilerMojo类上的execute方法第一行打断点,然后调试启动。断点就进来了。 
 

 
 
方式2 (无需下载源代码)
 
- 你可能也注意到右键maven的生命周期,会有一个调试的选项,但是点击调试是没有效果的。
 
 

 
- 这是因为你的项目中没有maven的源代码,无法添加断点,只要将maven插件的依赖引入即可。
 - 直接将
maven-compiler-plugin依赖添加到dependencies中。 
 
<dependencies>
  <dependency>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.1</version>
  </dependency>
</dependencies>
 
- 然后搜索
CompilerMojo类,在方法开始处打上断点。 
 

 
- 再调试启动maven的生命周期,断点就进来了,如果你不是源代码,而是class文件,可以点击右上角下载源代码。
 
 

 
Maven编译细节
 
- 前面有一些判断增量编译,是否需要编译的逻辑啥的,不是这次的重点,就略过了。最终走到编译器对象的执行编译方法。
 
 

 
- 然后到构建的构建编译命令这里
buildCompilerArguments.可以看到这里的args就是javac 命令后面的各种参数。 
 

 
- 最终通过javac执行参数+待编译的文件构建了一个命令行任务,执行编译,就是调用的javac。
 
 

 
- 如果执行不到编译步骤,记得先clean下。
 
 
IDEA 编译
 
找到编译入口
 
- idea是如何编译的呢,今天就来一探究竟。使用内部模式点击编译按钮,看看他关联的Action类。
 - 按住
ctrl + alt点击构建项目小锤子。可以看到他关联的是CompileDirtyAction类。逻辑也很简单,就是获取一个项目任务管理器,然后执行了一个构建所有模块的方法。 
 

 
- 它会通过一个线程池提交任务,然后走到
com.intellij.compiler.impl.CompileDriver#startup方法,然后执行到在外部程序中编译。 
 

 
 

 
- 可以看到运行的实际是一个java程序,我们看看这个java程序到底是什么,我把其他不重要的参数去掉,看看命令到底长啥样
 
 
"D:\Program Dev Kit\JDK\jdk8u412-b08\bin\java.exe"
  org.jetbrains.jps.cmdline.BuildMain 127.0.0.1 59075 25329350-c1e6-4849-9492-63e97408f1b2 D:/idea-source-code/intellij-community-idea-222.3345.118/system/idea/compile-server
 
- 可以看到这里实际执行了BuildMain这个类。由于这里是另外一个进程了,我们不能直接打断点到这里,代码是走不进来的。就需要使用构建调试和远程jvm调试了。
 
 
调试构建过程
 
- 调试构建过程是idea提供的一种机制,可以使我们在运行构建时去调试代码。如何知道有这个扩展点呢。
 - 在构建
OSProcessHandler系统命令行处理程序之前,有一段代码,判断myBuildProcessDebuggingEnabled,也就是是否开启调试构建过程。 
 

 
开启调试构建过程
 
- 现在知道了有这样一个判断,那要如何开启呢,我们就知道这个变量的赋值处,从下面的图片可以看到这个变量来源于这个一个action。找到关键key 
DebugBuildProcess 
 

 
- 在随处搜索中搜索操作(Action),输入
DebugBuildProcess,然后打开调试构建过程开关(需要调试的idea中)。 
 

 
设置调试构建过程监听端口号
 
- 可以看到监听的端口是从idea的注册表中获取的,如果没有,就会随机分配一个。 
   
 - 复制获取端口的key 
compiler.process.debug.port,按ctrl+shift+alt+/打开idea的注册表(被调试的idea),在其中搜索,然后修改端口号。 
 

 
新增远程jvm调试
 
- 新增一个远程jvm调试,将端口号改成刚刚的端口号。
 
 

 
调试构建
 
- 在被调试的idea中点击小锤子,开始构建项目。等待idea执行到在外呼程序构建时,也就是使用java命令运行
BuildMain之后,在被调试的idea的构建详情中就会显示等待调试,监听xxx端口。 
 

 
- 然后在有源代码的idea中运行远程Jvm调试程序。运行之后就可以看到进入了程序。
 
 

 
- 继续执行,会运行到
org.jetbrains.jps.incremental.IncProjectBuilder#runBuild方法。 
 

 
- 最终运行到
org.jetbrains.jps.incremental.java.JavaBuilder#compileJava,这里可以看到之中调用了javac来进行编译。 
 

 
- 这里并不是直接使用javac命令,而是使用了tools包下的javaCompiler,但其实他和javac是一样的。这里同样有编译java文件最重要的几个元素,执行javac的相关参数,以及要编译的类。
 
 
