← 返回 构建与部署图解 目录

☕ Java代码到JVM全链路

一个 .java 文件从写出到在服务器上跑起来,中间的 compile、build、jar、类加载、Spring Boot 启动

① 源码:你写的 Controller 长什么样

src/main/java/com/example/controller/UserController.java
📂 src/main
📂 java
📂 com/example
📂 controller
📄 UserController.java
📂 service
📄 UserService.java
📂 resources
📄 application.yml
@RestController @RequestMapping("/api/users") public class UserController { @Autowired private UserService userService; @GetMapping("/{id}") public User getUser(@PathVariable Long id) { return userService.findById(id); } }

这个文件是纯文本源码,JVM 不认识它。和前端的 .vue/.jsx 不同,Java 需要一个独立的编译步骤把 .java 变成 .class。

② 编译:javac 把 .java 变成了什么

javac(Java Compiler)把你写的 .java 源码编译成 字节码(Bytecode),也就是 .class 文件。

.java → .class 就像中文 → 世界语。 .java 是给人看的(你的母语),.class 是给 JVM 看的"世界语"。JVM 不管你用 Windows 还是 Linux,都能读懂字节码——这就是 Java "一次编译,到处运行"的秘密。
UserController.java @RestController public class ... { } 纯文本源码(给人看) javac 编译过程 1. 词法分析 2. 语法树AST 3. 生成字节码 UserController.class 0x CA 0xFE 0xBA 0xBE getfield #3 字节码(给JVM看) javap -c 可查看 反编译字节码指令 aload_0, invokevirtual #5
.java(人类可读)→ javac → .class(字节码,JVM可读)。.class 文件以 0xCAFEBABE 开头(Java 的魔法数字)
UserController.class(用 javap -c 反编译看到的字节码)
public class com.example.controller.UserController { public com.example.model.User getUser(java.lang.Long); Code: 0: aload_0 1: getfield #1 // Field userService 4: aload_1 5: invokeinterface #2, 2 // InterfaceMethod UserService.findById 10: areturn } // 字节码指令:aload=加载变量到操作数栈,getfield=获取字段,invokeinterface=调用接口方法
字节码不是机器码!字节码是一种中间格式,还需要 JVM 在运行时翻译成 CPU 真正能执行的机器码(JIT 编译器做的事)。所以 Java 比 C/C++(直接编译成机器码)多了一层翻译,启动慢一点,但跨平台。

③ Maven 打包:从一堆 .class 到一个可运行的产物

你不可能只写一个文件——一个 Spring Boot 项目可能有几十上百个 .java。Maven 负责把它们全部编译、加上依赖库、打成一个可部署的包。

mvn clean compile 所有 .java → target/classes/*.class mvn package target/myapp.jar
Maven 打包全流程 src/main/java/ *.java(几十个) javac target/classes/ *.class(编译后) src/main/resources/ application.yml 等 Maven 依赖 spring-boot-*.jar mvn package 📦 target/ myapp-0.0.1.jar fat jar(包含一切) java -jar myapp.jar 1. JVM 启动 2. 类加载器加载 .class 3. Spring Boot 启动 Tomcat
Maven 把编译后的 .class + 配置文件 + 所有依赖库一起打成一个 fat jar

Spring Boot 的 fat jar 是什么

传统 Java Web 项目打成 .war 文件,需要外部 Tomcat 来运行。Spring Boot 改成了 fat jar(胖 jar)——把 Tomcat 本身也打进 jar 包里,所以 java -jar 就能直接启动,不需要额外装 Tomcat。

terminal
# 编译打包 $ mvn clean package [INFO] --- maven-compiler-plugin:3.11.0:compile --- # 编译 .java → .class [INFO] --- spring-boot-maven-plugin:3.2.0:repackage --- # 打成 fat jar [INFO] Building jar: target/myapp-0.0.1-SNAPSHOT.jar # ✅ 最终产物 # 运行 $ java -jar target/myapp-0.0.1-SNAPSHOT.jar . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v3.2.0)

④ jar 包内部结构:解压后能看到什么

jar 本质上就是一个 zip 压缩包。你可以用 jar tf 或解压工具看里面的内容。

myapp-0.0.1-SNAPSHOT.jar # 解压后看到: ├── META-INF/ │ ├── MANIFEST.MF # 清单文件:指定 Main-Class(入口类) │ └── maven/ # Maven 元数据(pom.xml 等) ├── BOOT-INF/ │ ├── classes/ # ✅ 你写的代码在这里 │ │ ├── com/example/controller/ │ │ │ └── UserController.class # 编译后的字节码 │ │ ├── com/example/service/ │ │ │ └── UserService.class │ │ └── application.yml # 配置文件 │ └── lib/ # ✅ 所有依赖库在这里 │ ├── spring-boot-3.2.0.jar # Spring Boot 核心 │ ├── spring-context-6.1.0.jar # Spring IoC 容器 │ ├── spring-web-6.1.0.jar # Spring MVC │ ├── tomcat-embed-core-10.1.0.jar # 内嵌 Tomcat! │ ├── jackson-databind-2.16.0.jar # JSON 序列化 │ ├── mybatis-spring-3.0.3.jar │ ├── mysql-connector-j-8.2.0.jar # MySQL 驱动 │ └── ... 还有 100+ 个依赖 jar └── org/springframework/boot/loader/ └── JarLauncher.class # Spring Boot 启动器
fat jar 的"胖"在哪?你的代码可能只有几十 KB,但 BOOT-INF/lib/ 里的依赖库加起来可能有 50-100MB。这就是为什么 Spring Boot jar 通常很大——它包含了运行所需的一切。

⑤ JVM 类加载:.class 怎么进入内存的

java -jar 启动时,JVM 会按需把 .class 文件加载到内存里。不是一次性全部加载——用到哪个类才加载哪个。

类加载就像图书馆借书。 jar 包是整个图书馆(书架上有几万本书),类加载器是图书管理员。你(程序)说"我要 UserController 这本书",管理员去书架上找到它,检查(验证)它没被篡改,然后放到你的书桌(方法区)上。不是一次性把所有书都搬到桌上。
类加载的 5 个阶段 ① 加载 Loading 读 .class 到内存 ② 验证 Verification 字节码安全检查 ③ 准备 Preparation 分配内存+默认值 ④ 解析 Resolution 符号引用→直接引用 ⑤ 初始化 Init 执行 static 块 JVM 方法区(Method Area)— 类信息存放的地方 ⑤ 之后:JIT 编译器把热点字节码翻译成机器码 → CPU 执行 JIT(Just-In-Time)= 运行时编译,把频繁执行的字节码优化成机器码缓存起来
类加载 5 步:加载→验证→准备→解析→初始化。之后 JIT 编译器在运行时把热点代码翻译成机器码

Spring Boot 的三级类加载器

类加载器负责加载什么类比
Bootstrap ClassLoaderJRE 核心类(java.lang.*, java.util.*)图书馆的"基础藏书"——人人都需要的字典
App ClassLoader你的代码 + Maven 依赖你的"私人书架"——你的项目和 jar 依赖
LaunchedURLClassLoaderSpring Boot 特有:从 fat jar 的 BOOT-INF/classes/ 和 lib/ 加载"移动图书馆"——从打包好的箱子里取书
双亲委派机制:类加载器收到请求时,先问上级"你加载过这个类吗?",上级加载了就用上级的。这保证了核心类(如 java.lang.String)不会被你的代码覆盖——安全。

⑥ Spring Boot 启动:从 main 方法到能接收请求

src/main/java/com/example/Application.java
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }

java -jar 做的第一件事:找到 MANIFEST.MF 里指定的 Main-Class,执行它的 main() 方法。

JVM 启动 执行 main() SpringApplication.run() 创建 IoC 容器
扫描 @Component/@Service/@RestController 创建 Bean 实例 + 注入依赖 启动内嵌 Tomcat 注册 @GetMapping 等路由 ✅ 开始接收 HTTP 请求
Spring Boot 启动就像开一家餐厅:
1. JVM 启动 = 房东开门(操作系统启动进程)
2. main() = 店长到场
3. IoC 容器 = 成立管理层
4. 扫描 @Component = 招聘所有员工(Controller=服务员,Service=厨师,Repository=采购员)
5. @Autowired 注入 = 给厨师分配采购员
6. 启动 Tomcat = 开店营业
7. 注册路由 = 挂菜单牌(GET /api/users → 找 UserController)
怎么看启动过程?控制台那些 Spring Boot 启动日志(绿色的日志行)就是每一步的输出。如果启动慢,在 application.ymllogging.level.org.springframework=DEBUG 可以看到更详细的加载过程。

⑦ Java 构建 vs 前端构建:核心区别

对比维度前端(Vue/React)Java(Spring Boot)
源码文件.vue / .jsx.java
编译工具Vite/Webpack + Babel/vue-compilerjavac + Maven
编译产物.js + .css(浏览器直接执行).class 字节码(需要 JVM 翻译成机器码)
打包格式dist/ 目录(多个 .js + index.html)一个 .jar 文件(zip 格式)
运行环境浏览器(用户的电脑)JVM(服务器上)
依赖管理package.json + npm(node_modules/)pom.xml + Maven(~/.m2/ 本地仓库)
依赖是否打进产物✅ 打进去(bundle 里包含 vue/react 库代码)✅ fat jar 里包含所有依赖(BOOT-INF/lib/)
部署方式把 dist/ 放到 Nginx 静态目录java -jar myapp.jar(不需要外部容器)
更新方式替换 dist/ 文件,用户刷新浏览器重启 JVM 进程(或热部署)
SourceMap.js.map(映射回 .vue/.jsx)不需要(.jar 里保留 .class,IDE 直接调试 .java)
代码混淆有(压缩 + 变量名变 a,b,c)通常不做(后端代码用户看不到)
最大的思维差异:前端的代码发给用户浏览器执行——用户能看到你的源码(F12),所以要压缩混淆。Java 的代码在服务器上运行——用户永远看不到你的 .class,所以不需要混淆,但也意味着出了问题只能在服务器上调试。

⑧ FAQ

为什么 Java 不能像 JavaScript 那样直接运行源码?

因为 Java 是静态编译型语言——必须先编译成字节码,JVM 才能运行。JavaScript 是解释型语言——浏览器直接逐行解释执行(虽然有 JIT,但不需要预编译步骤)。

好处:Java 编译时就能发现类型错误、语法错误(编译报错),不用等运行时才暴露。坏处:每次改代码都要等编译(虽然 IDEA 的增量编译很快)。

.class 文件可以直接反编译回 .java 吗?安全吗?

可以!工具如 JD-GUI、CFR、Procyon 能把 .class 反编译回接近原始的 .java 代码。所以如果你不给 jar 加密,别人解压你的 jar 就能看到业务逻辑。

要保护代码可以用混淆器(ProGuard)——把类名变量名变成 a/b/c,让反编译出来的代码几乎不可读。但不像前端的压缩那么彻底。

为什么 Spring Boot 启动那么慢(几秒到几十秒)?前端启动秒级

因为 Spring Boot 启动时要做很多事情:创建 IoC 容器、扫描所有类、创建所有 Bean、注入依赖、启动 Tomcat、注册路由。每个 Bean 都要实例化(可能还连数据库、加载配置)。

前端开发时 Vite 不打包,即时编译返回,所以很快。生产构建也要几秒到几十秒(和项目大小有关)。

GraalVM Native Image 可以把 Spring Boot 编译成原生可执行文件,启动时间降到几十毫秒——但编译时间更长(几分钟)。

Maven 的 target 目录和 dist 目录有什么区别?

本质一样——都是构建产物的输出目录。target/ 是 Maven 约定的,dist/ 是前端(Vite/Webpack)约定的。里面放的都是最终可部署的文件。

Maven 的 target/classes/ 是编译后的 .class,target/xxx.jar 是打包后的 jar。前端的 dist/assets/ 是打包后的 JS/CSS。

Spring Boot 为什么把 Tomcat 打进 jar 里?不是应该外部部署吗?

传统 Java Web 打成 .war 部署到外部 Tomcat。Spring Boot 理念是"约定优于配置" + "自包含"——把 Tomcat 嵌入 jar 里,java -jar 就能启动,不需要运维额外装 Tomcat。

好处:部署简单(一个 jar 走天下)、Docker 友好、微服务拆分方便。坏处:jar 文件较大、多个服务各自有 Tomcat(内存开销稍大)。

← 返回 构建与部署图解 目录