javac详解 idea maven内部编译原理 自制编译器

起因

  • 不知道大家在开发中,有没有过下面这些疑问。有的话,今天就一次解答清楚。
  1. 如何使用javac命令编译一个项目?
  2. java或者javac的一些参数到底有什么用?
  3. idea或者maven是如何编译java项目的?(你可能猜测底层是javac,但是你没有证据)。
  4. 自己能不能写一个程序,去编译一个项目。

Javac命令详解

简单使用

  • javac Hello.java
public class Hello {public static void main(String[] args) {System.out.println("Hello World!");}
}
  • java Hello
  • 上面的命令是通过javac命令编译一个java文件,它会生成一个Hello.class文件。然后使用java命令运行Hello程序。

带包名编译运行

  • 正常使用 javac Hello.java编译
package com.maple;public class Hello {public static void main(String[] args) {System.out.println("Hello World!");}
}
  • 然后使用java com.maple.Hello运行。但是你会发现报错了。
java Hello
错误: 找不到或无法加载主类 Hellojava com.maple.Hello
错误: 找不到或无法加载主类 com.maple.Hello
  • 需要将编译之后的Hello.class文件移动到目录com/maple中去,并在com的同级目录执行java com.maple.Hello即可正常执行。
  • – com
    • – maple
      • Hello.class
  • 在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文件输出目录。
  • 最终效果如下

image.png

  • 同时如果有外部依赖,可以使用-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;/*** java编译工具* 1.编译MyJavaCompilerTool工具* <p>* javac -encoding utf-8 "D:/Program Project/Project List/dev-note/base/src/main/java/com/maple/compiler/MyJavaCompilerTool.java"* <p>* 或者在cd进入base模块下,进入主路径:D:\Program Project\Project List\dev-note\base\src\main\java* <p>* javac -encoding utf-8 com/maple/compiler/MyJavaCompilerTool.java* <p><p>* 2.使用原始命令构建项目* java com.maple.compiler.MyJavaCompilerTool "D:\Program Project\Project List\dev-note\base" -cp "D:\Program Project\Project List\dev-note\jsr269\src\main\java"* <p>* 3.设置环境变量,快捷调用* set myjavac=java -cp "D:\Program Project\Project List\dev-note\base\src\main\java" com.maple.compiler.MyJavaCompilerTool* <p>* 4.调用命令* %myjavac%* <p>* 5.或者使用环境变量* 将myjavac.bat文件所在的目【D:\Program Project\Project List\dev-note\base\src\main\java\com\maple\compiler】添加到环境变量中* 任意路径执行 【myjavac】 调用** @author maple* @since 2024/07/19*/
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);// 获取所有Java文件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>() {@Overridepublic 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) {// 避免非项目路径去找java文件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()

image.png

Debugger(调试) Maven插件

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

image.png

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

image.png

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

image.png

  • 这是因为你的项目中没有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类,在方法开始处打上断点。

image.png

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

image.png

Maven编译细节

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

image.png

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

image.png

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

image.png

  1. 如果执行不到编译步骤,记得先clean下。

IDEA 编译

找到编译入口

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

image.png

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

image.png

  • 后面又是启一个调度线程,然后启动构建程序。

image.png

  • 可以看到运行的实际是一个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,也就是是否开启调试构建过程

image.png

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

image.png

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

image.png

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

image.png

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

image.png

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

image.png

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

image.png

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

image.png

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

image.png

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

image.png

  • idea中的编译流程就介绍完毕了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/48458.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【一刷《剑指Offer》】面试题 47:不用加减乘除做加法

力扣对应题目链接&#xff1a;LCR 190. 加密运算 - 力扣&#xff08;LeetCode&#xff09; 牛客对应题目链接&#xff1a;不用加减乘除做加法_牛客题霸_牛客网 (nowcoder.com) 一、《剑指Offer》对应内容 二、分析题目 sumdataA⊕dataB 非进位和&#xff1a;异或运…

Unity UGUI 之 Graphic Raycaster

本文仅作学习笔记与交流&#xff0c;不作任何商业用途 本文包括但不限于unity官方手册&#xff0c;唐老狮&#xff0c;麦扣教程知识&#xff0c;引用会标记&#xff0c;如有不足还请斧正 首先手册连接如下&#xff1a; Unity - Manual: Graphic Raycaster 笔记来源于&#xff…

无人车技术浪潮真的挡不住了~

正文 无人驾驶汽车其实也不算是新鲜玩意了&#xff0c;早在十年前大家都开始纷纷投入研发&#xff0c;在那时就已经蠢蠢欲动&#xff0c;像目前大部分智驾系统和辅助驾驶系统都是无人驾驶系统的一个中间过度版本&#xff0c;就像手机进入智能机时代的中间版本。 然而前段时间突…

SpringBoot 介绍和使用(详细)

使用SpringBoot之前,我们需要了解Maven,并配置国内源(为什么要配置这些,下面会详细介绍),下面我们将创建一个SpringBoot项目"输出Hello World"介绍. 1.环境准备 ⾃检Idea版本: 社区版: 2021.1 -2022.1.4 专业版: ⽆要求 如果个⼈电脑安装的idea不在这个范围, 需要…

LeetCode 热题 HOT 100 (001/100)【宇宙最简单版】

【链表】 No. 0160 相交链表 【简单】&#x1f449;力扣对应题目指路 希望对你有帮助呀&#xff01;&#xff01;&#x1f49c;&#x1f49c; 如有更好理解的思路&#xff0c;欢迎大家留言补充 ~ 一起加油叭 &#x1f4a6; 欢迎关注、订阅专栏 【力扣详解】谢谢你的支持&#x…

搜维尔科技:【产品推荐】Euleria Health Riablo 运动功能训练与评估系统

Euleria Health Riablo 运动功能训练与评估系统 Riablo提供一种创新的康复解决方案&#xff0c;将康复和训练变得可激励、可衡量和可控制。Riablo通过激活本体感觉&#xff0c;并通过视听反馈促进神经肌肉的训练。 得益于其技术先进和易用性&#xff0c;Riablo是骨科、运动医…

jmeter部署

一、windows环境下部署 1、安装jdk并配置jdk的环境变量 (1) 安装jdk jdk下载完成后双击安装包&#xff1a;无限点击"下一步"直到完成&#xff0c;默认路径即可。 (2) jdk安装完成后配置jdk的环境变量 找到环境变量中的系统变量&#xff1a;此电脑 --> 右键属性 …

C语言:温度转换

1.题目&#xff1a;实现摄氏度&#xff08;Celsius&#xff09;和华氏度&#xff08;Fahrenheit&#xff09;之间的转换。 输入一个华氏温度&#xff0c;输出摄氏温度&#xff0c;结果保留两位小数。 2.思路&#xff1a;&#xff08;这是固定公式&#xff0c;其中 F 是华氏度&a…

【C语言】详解结构体(下)(位段)

文章目录 前言1. 位段的含义2. 位段的声明3. 位段的内存分配&#xff08;重点&#xff09;3.1 存储方向的问题3.2 剩余空间利用的问题 4. 位段的跨平台问题5. 位段的应用6. 总结 前言 相信大部分的读者在学校或者在自学时结构体的知识时&#xff0c;可能很少会听到甚至就根本没…

STM32实战篇:按键(外部输入信号)触发中断

功能要求 将两个按键分别与引脚PA0、PA1相连接&#xff0c;通过按键按下&#xff0c;能够触发中断响应程序&#xff08;不需明确功能&#xff09;。 代码流程如下&#xff1a; 实现代码 #include "stm32f10x.h" // Device headerint main() {//开…

JUC并发编程01-基础概念

概念 进程 进程可以视为程序的一个实例&#xff0c;进程就是用来加载指令、管理内存、管理I0 线程 一个进程内可以有多个线程&#xff0c;一个线程就是一个指令流。 在Java中&#xff0c;线程作为最小调度单位&#xff0c;进程作为资源分配的最小单位&#xff0c;可以说进程…

Mysql数据库第二次作业

(1)显示所有职工的基本信息。 mysql> select * from t_worker; (2)查询所有职工所属部门的部门号&#xff0c;不显示重复的部门号。 mysql> select distinct department_id from t_worker; (3)求出所有职工的人数。 mysql> select count(1) from t_worker; (4)列…

Figma 中文版指南:获取和安装汉化插件

Figma是一种主流的在线团队合作设计工具&#xff0c;也是一种基于 Web 端的设计工具。在当今的设计时代&#xff0c;Figma 的使用满足了每个人的设计需求&#xff0c;不仅可以实现在线编辑&#xff0c;还可以方便日常管理&#xff0c;有效提高工作效率。然而&#xff0c;相信很…

分页查询与分页条件查询

--------------- 无PageHelper插件分页查询 1.创建PageBean实体类 Data NoArgsConstructor AllArgsConstructor public class PageBean<T> {private Long total;//总条数private List<T> items;//当前页数据集合 }类型安全性 泛型&#xff1a;提供了编译时的类型…

【Apache Doris】周FAQ集锦:第 15 期

【Apache Doris】周FAQ集锦&#xff1a;第 15 期 SQL问题数据操作问题运维常见问题其它问题关于社区 欢迎查阅本周的 Apache Doris 社区 FAQ 栏目&#xff01; 在这个栏目中&#xff0c;每周将筛选社区反馈的热门问题和话题&#xff0c;重点回答并进行深入探讨。旨在为广大用户…

JMeter:BeanShell到JSR223迁移中的注意事项

前言 在之前的文章JMeter&#xff1a;BeanShell向JSR223迁移过程遭遇的java标准库不可用问题-如何切换JDK版本中引用了一段使用BeanShell对入参进行加密的脚本&#xff0c;迁移到JSR223&#xff0c;虽然更换JDK后编译通过&#xff0c;看似也可以执行了&#xff0c;但是其实那段…

windows USB 设备驱动开发-开发Type C接口的驱动程序(二)

编写 USB Type C 连接器驱动程序 在以下情况下&#xff0c;需要编写 USB Type-C 连接器驱动程序&#xff1a; 如果 USB Type-C 硬件能够处理电源输送 (PD) 状态机。 否则&#xff0c;请考虑编写 USB Type C 端口控制器驱动程序&#xff1b; 如果硬件没有嵌入式控制器。 否则&…

(10)深入理解pandas的核心数据结构:DataFrame高效数据清洗技巧

目录 前言1. DataFrame数据清洗1.1 处理缺失值&#xff08;NaNs&#xff09;1.1.1 数据准备1.1.2 读取数据1.1.3 查找具有 null 值或缺失值的行和列1.1.4 计算每列缺失值的总数1.1.5 删除包含 null 值或缺失值的行1.1.6 利用 .fillna&#xff08;&#xff09; 方法用Portfolio …

Python本地安装whl文件详解与高级pip命令技巧

有些情况我们pip一下包的时候会报错&#xff0c;可能因为延时或许其他不兼容的情况&#xff0c;这时候我们可以通过去网上下载该包的原文件&#xff0c;进行本地物理pip &#xff0c;在安装网上下载的whl之前&#xff0c;先明白不同后缀的差异&#xff1b;whl下载网址&#xff…

解答word图标变白

把WPS卸载了之后就变成白色了&#xff0c;然后在注册表中把word的地址改成office word的地址之后图标变成这样了&#xff0c;怎么办 1.winR打开命令提示符 2.输入regedit&#xff0c;打开注册表编辑器 3.找到下面这个路径 计算机\HKEY_CLASSES_ROOT\Word.Document.8\Defaul…