Skip to content

第四部分:理解 Java 工程 ​

4.1 本章目标 ​

读完本章后,你应该能:

  1. 理解 Maven 是什么,以及它和 npm/pnpm 的对应关系
  2. 看懂 pom.xml 的结构
  3. 理解 dependency 的 scope 和作用
  4. 知道 Maven 生命周期各阶段做什么
  5. 理解 Java 的 package 和 import 机制
  6. 知道 jar 包是什么
  7. 理解 application.yml 的基本结构
  8. 看懂 Java 程序的日志和异常堆栈

4.2 为什么需要学习这一章 ​

如果说前两章学的是"怎么写 Java 代码",这一章学的是"怎么组织 Java 项目"。不学这一章,你看到别人的 Java 项目会一脸茫然——不知道 pom.xml 在干嘛,不知道为什么目录那么深,不知道怎么跑起来。


4.3 Maven 是什么 ​

4.3.1 一句话类比 ​

Maven 概念npm/pnpm 类比
Maven 本身npm / pnpm
pom.xmlpackage.json
本地仓库 ~/.m2/repository/node_modules
Maven Centralnpm registry
dependencydependencies
plugindevDependencies 中的构建工具

4.3.2 Maven 负责三件事 ​

  1. 依赖管理 — 下载 jar 包(相当于 npm install)
  2. 构建 — 编译、测试、打包(相当于 npm run build)
  3. 项目标准化 — 统一目录结构(npm 不强制项目结构,Maven 强制)

4.3.3 安装 Maven ​

bash
# macOS
brew install maven

# 验证
mvn --version

4.4 pom.xml 怎么看 ​

4.4.1 一个最小的 pom.xml ​

xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- 项目坐标(唯一的身份标识) -->
    <groupId>com.example</groupId>      <!-- 组织名,类似 npm scope -->
    <artifactId>study-tracker</artifactId>  <!-- 项目名,类似 package name -->
    <version>0.0.1-SNAPSHOT</version>    <!-- 版本号 -->
    <packaging>jar</packaging>           <!-- 打包方式:jar / war -->

    <!-- 项目基本信息 -->
    <name>study-tracker</name>
    <description>学习时间记录系统</description>

    <!-- 属性配置 -->
    <properties>
        <java.version>17</java.version>
    </properties>
</project>

4.4.2 对比 package.json ​

json
{
    "name": "@example/study-tracker",   // groupId + artifactId
    "version": "0.0.1",                // version
    "description": "学习时间记录系统"
}

4.4.3 项目坐标(GAV) ​

Maven 用三个坐标唯一确定一个项目:

  • groupId — 组织或公司域名倒写(如 com.example、org.springframework.boot)
  • artifactId — 项目名(如 study-tracker、spring-boot-starter-web)
  • version — 版本号

这和 npm 的 @scope/package-name@version 是同一个概念。


4.5 dependency 是什么 ​

4.5.1 添加依赖 ​

xml
<dependencies>
    <!-- Spring Boot Web 起步依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>4.1.2</version>
    </dependency>

    <!-- MySQL 驱动 -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <version>9.7.0</version>
        <scope>runtime</scope>  <!-- 运行时才需要 -->
    </dependency>

    <!-- Lombok:简化代码 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.48</version>
        <scope>provided</scope>  <!-- 编译时需要,不打入最终包 -->
    </dependency>

    <!-- 测试依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <version>4.1.2</version>
        <scope>test</scope>  <!-- 仅测试时使用 -->
    </dependency>
</dependencies>

版本由父 POM 管理:实际项目继承 spring-boot-starter-parent 后,Spring Boot、MySQL 驱动、Lombok 和测试依赖的版本都由父 POM 决定,不需要逐个写 <version>。上面的写法只是为了让依赖坐标完整可见。

Spring Boot 3 → 4 迁移提示:Boot 4 基于 Spring Framework 7、Jakarta EE 11 和 Jackson 3,starter 做了模块化(如 spring-boot-starter-webmvc)。Java 17 仍是最低要求,但新项目建议直接用 21/25 LTS。从 3.x 升级时,MyBatis-Plus 必须换成 mybatis-plus-spring-boot4-starter;如果代码里直接 import 了 com.fasterxml.jackson.*,要迁移到 Jackson 3 的 tools.jackson.*,并跑一遍集成测试再上线。

对比 npm:

bash
npm install express          # 相当于添加 dependency
npm install -D typescript    # 相当于 scope=provided(开发时用)
npm install -D jest          # 相当于 scope=test

4.5.2 dependency scope(依赖范围) ​

scope含义npm 类比什么时候用
compile(默认)编译、测试、运行都需要dependencies大部分情况
runtime编译不需要,运行需要dependencies数据库驱动
provided编译和测试需要,运行不需要devDependenciesLombok、Servlet API
test仅测试需要devDependenciesJUnit、Mockito

4.5.3 依赖传递 ​

Maven 会自动下载依赖的依赖(传递依赖),和 npm 一样。

但如果出现版本冲突,Maven 的规则是"最短路径优先"和"先声明优先"。


4.6 Maven 生命周期 ​

4.6.1 三个标准生命周期 ​

Maven 有三个独立的生命周期:

生命周期用途常用阶段
clean清理项目clean:删除 target 目录
default构建项目compile、test、package、install
site生成项目文档很少用

4.6.2 default 生命周期关键阶段 ​

validate → compile → test → package → verify → install → deploy
阶段作用npm 类比
compile编译源代码tsc
test运行测试npm test
package打包(生成 jar 文件)npm run build
install安装到本地仓库npm link(近似)
deploy部署到远程仓库npm publish

重要: 执行后面的阶段会自动执行前面的阶段。比如 mvn package 会先执行 compile 和 test。

4.6.3 常用命令 ​

bash
mvn clean                    # 清空 target 目录
mvn compile                  # 编译
mvn test                     # 运行测试
mvn package                  # 打包成 jar
mvn clean package            # 清空 + 打包
mvn spring-boot:run          # 运行 Spring Boot 项目
mvn clean package -DskipTests  # 跳过测试打包

4.7 package 和 import ​

4.7.1 package ​

Java 的 package 是代码组织方式,和 TypeScript 的 namespace 或 Python 的 package 类似。

java
// 文件:src/main/java/com/example/studytracker/controller/StudyController.java
package com.example.studytracker.controller;  // 声明所属包

public class StudyController {
    // ...
}

规则:

  • package 名称必须和目录结构一致
  • 约定使用反向域名格式(如 com.example.studytracker)
  • 同一个包下的类可以直接互相访问(不需要 import)

4.7.2 import ​

java
package com.example.studytracker.controller;

import com.example.studytracker.service.StudyService;  // 导入其他包的类
import java.util.List;                                   // 导入 JDK 的类
import java.util.*;                                      // 导入包下所有类(不推荐)

public class StudyController {
    private StudyService studyService;  // 使用导入的类
}

对比:

typescript
// TypeScript
import { StudyService } from '../service/study.service';
python
# Python
from service.study_service import StudyService

Java 的 import 不是"导入文件",而是"导入类"。 它和 TypeScript 的 import 概念一致,但语法不同。

4.7.3 不需要 import 的情况 ​

  • 同一个 package 下的类
  • java.lang 包下的类(如 String、Integer、System),自动导入

4.8 jar 包是什么 ​

4.8.1 一句话 ​

jar(Java Archive)就是 Java 的"打包产物",本质上是一个 zip 文件,里面包含:

  • 编译后的 .class 文件
  • 资源文件(配置文件、图片等)
  • META-INF/MANIFEST.MF(元数据)

4.8.2 类比 ​

概念JavaNode.jsPython
打包产物.jardist/ 目录.whl 文件
可执行文件Spring Boot fat jarnode dist/index.jspython -m app
依赖管理打入 jar 或声明式node_modulessite-packages

4.8.3 Spring Boot 的 fat jar ​

Spring Boot 打包时会把所有依赖(Tomcat、Spring、MySQL 驱动等)都打入一个 jar 包,称为 fat jar(或 uber jar)。

bash
# 打包
mvn clean package

# 运行(不需要额外配置,直接 java -jar)
java -jar target/study-tracker-0.0.1-SNAPSHOT.jar

这和 Node.js 把依赖打入 node_modules 不同——Java 的 fat jar 是一个单一文件,可以直接部署。


4.9 Java 项目如何启动 ​

4.9.1 启动类 ​

java
// 文件:src/main/java/com/example/studytracker/StudyTrackerApplication.java
package com.example.studytracker;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication  // 这个注解包含了三个注解:
                        // @Configuration、@EnableAutoConfiguration、@ComponentScan
public class StudyTrackerApplication {
    public static void main(String[] args) {
        SpringApplication.run(StudyTrackerApplication.class, args);
    }
}

对比 NestJS:

typescript
// NestJS main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
    const app = await NestFactory.create(AppModule);
    await app.listen(3000);
}
bootstrap();

4.9.2 三种启动方式 ​

bash
# 方式一:Maven 插件启动(开发时常用)
mvn spring-boot:run

# 方式二:打包后启动(生产环境)
mvn clean package
java -jar target/study-tracker-0.0.1-SNAPSHOT.jar

# 方式三:IDEA 中直接运行 main 方法(开发时最常用)
# 右键 StudyTrackerApplication.java → Run

4.10 环境变量和配置文件 ​

4.10.1 application.yml 基础 ​

yaml
# 文件:src/main/resources/application.yml

# 服务器配置
server:
  port: 8080

# 数据库配置
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/study_tracker
    username: root
    password: ${DB_PASSWORD:root}  # 环境变量优先,默认 root
    driver-class-name: com.mysql.cj.jdbc.Driver

# 自定义配置
app:
  jwt:
    secret: ${JWT_SECRET:my-secret-key}
    expiration: 86400000  # 24小时,单位毫秒
  python-agent:
    url: http://localhost:8000
    timeout: 30000

对比:

概念JavaNode.jsPython
配置文件application.yml.env + config.ts.env + config.py
环境变量${DB_PASSWORD:root}process.env.DB_PASSWORDos.environ.get("DB_PASSWORD")
多环境application-dev.yml.env.developmentconfig/dev.py

4.10.2 多环境配置 ​

application.yml           # 公共配置
application-dev.yml       # 开发环境
application-prod.yml      # 生产环境

通过 spring.profiles.active 指定激活哪个:

yaml
# application.yml
spring:
  profiles:
    active: dev  # 激活 application-dev.yml

或在启动时指定:

bash
java -jar app.jar --spring.profiles.active=prod

4.10.3 在代码中读取配置 ​

java
// 方式一:@Value 注解(简单值)
@Value("${app.jwt.secret}")
private String jwtSecret;

// 方式二:@ConfigurationProperties(批量读取)
@ConfigurationProperties(prefix = "app.python-agent")
public class PythonAgentConfig {
    private String url;
    private int timeout;
    // getter / setter
}

4.11 日志和异常堆栈怎么看 ​

4.11.1 Spring Boot 默认日志 ​

Spring Boot 使用 SLF4J + Logback 作为日志框架:

java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@RestController
public class StudyController {
    private static final Logger log = LoggerFactory.getLogger(StudyController.class);

    @GetMapping("/api/study/today")
    public Result<?> getToday() {
        log.info("查询今日学习记录,用户ID:{}", getCurrentUserId());
        // ...
    }
}

日志级别(从高到低):

ERROR > WARN > INFO > DEBUG > TRACE

配置日志级别:

yaml
# application.yml
logging:
  level:
    com.example.studytracker: DEBUG    # 自己的代码 DEBUG
    org.springframework: WARN          # Spring 框架只输出 WARN 以上

4.11.2 看懂异常堆栈 ​

2026-07-25 10:30:00.123 ERROR 12345 --- [nio-8080-exec-1] c.e.s.controller.StudyController        : 查询学习记录失败

java.lang.NullPointerException: Cannot invoke "String.length()" because "name" is null
    at com.example.studytracker.service.StudyService.getUserStudyTime(StudyService.java:45)  ← 根本原因
    at com.example.studytracker.controller.StudyController.getToday(StudyController.java:30)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(...)
    ...

阅读方法:

  1. 第一行是异常类型和消息
  2. 找到第一个 你自己的代码(不是 JDK 或框架的),通常是问题根源
  3. 上面例子中:StudyService.java:45 是出问题的地方
  4. 从下往上看调用链,了解请求是如何到达出错位置的

4.11.3 对比日志 ​

概念JavaNode.jsPython
日志框架SLF4J + LogbackWinston / Pinologging
错误堆栈异常堆栈(极其详细)Error stack traceTraceback
日志格式可配置(JSON、文本)可配置可配置

4.12 完整示例:创建一个 Maven 项目 ​

4.12.1 手动创建 ​

bash
# 创建目录结构
mkdir -p hello-maven/src/main/java/com/example
mkdir -p hello-maven/src/main/resources
mkdir -p hello-maven/src/test/java/com/example

# 创建 pom.xml
cat > hello-maven/pom.xml << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>hello-maven</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.10.1</version>
        </dependency>
    </dependencies>
</project>
EOF

# 创建 Main.java
cat > hello-maven/src/main/java/com/example/Main.java << 'EOF'
package com.example;

import com.google.gson.Gson;

public class Main {
    public static void main(String[] args) {
        Gson gson = new Gson();
        String json = gson.toJson(new Person("Alice", 25));
        System.out.println(json);
    }

    static class Person {
        String name;
        int age;
        Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
    }
}
EOF

# 编译并运行
cd hello-maven
mvn compile
mvn exec:java -Dexec.mainClass="com.example.Main"

4.13 在 Spring Boot 项目中的实际位置 ​

概念实际位置
pom.xml项目根目录
启动类src/main/java/.../XxxApplication.java
配置文件src/main/resources/application.yml
编译产物target/
日志输出控制台 + logs/ 目录(如果配置了文件输出)

4.14 常见错误 ​

错误一:依赖下载失败 ​

bash
# 症状:mvn compile 报错 "Could not resolve dependencies"
# 原因:网络问题或 Maven 仓库配置

# 解决:检查 ~/.m2/settings.xml 中的镜像配置
# 国内常用阿里云镜像:
xml
<mirror>
    <id>aliyun</id>
    <mirrorOf>central</mirrorOf>
    <name>Aliyun Maven Mirror</name>
    <url>https://maven.aliyun.com/repository/public</url>
</mirror>

错误二:找不到主类 ​

bash
# 症状:java -jar target/app.jar 报错 "no main manifest attribute"
# 原因:没有配置打包插件

# 解决:在 pom.xml 添加 Spring Boot Maven Plugin
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

错误三:端口被占用 ​

bash
# 症状:启动报错 "Port 8080 was already in use"
# 解决:修改端口
yaml
# application.yml
server:
  port: 8081

4.15 三道小练习 ​

练习 1:pom.xml 解读 ​

看下面的 pom.xml 片段,回答:

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>
  1. 第一个依赖的 scope 是什么?
  2. 为什么 MySQL 驱动的 scope 是 runtime?
  3. 这两个依赖对应的 npm 安装命令是什么?

练习 2:创建 Maven 项目 ​

用命令行创建一个 Maven 项目,添加 Gson 依赖,写一个程序把 Map<String, Object> 转成 JSON 并打印。

练习 3:日志分析 ​

下面是一段异常堆栈,找出问题出在哪个文件的哪一行:

java.lang.NullPointerException
    at com.example.service.UserService.getUser(UserService.java:32)
    at com.example.controller.UserController.getUser(UserController.java:18)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

4.16 参考答案 ​

练习 1 ​

  1. compile(默认)
  2. 因为编译时不需要 MySQL 驱动类,运行时连接数据库才需要
  3. npm install spring-boot-starter-web 和 npm install mysql2

练习 2 ​

略(参考 4.12 节的完整示例)

练习 3 ​

问题出在 UserService.java 的第 32 行,UserController.java 第 18 行调用了它。


4.17 本章速查表 ​

概念一句话npm 类比
pom.xml项目配置文件package.json
dependency依赖声明dependencies
Maven Central中央仓库npm registry
~/.m2/repository/本地仓库node_modules
mvn clean清空构建产物rm -rf dist/
mvn compile编译tsc
mvn package打包成 jarnpm run build
jarJava 打包产物dist/ 目录
java -jar app.jar运行 Java 程序node dist/index.js
application.yml配置文件.env + config.ts
@Value读取配置process.env.XXX

4.18 是否需要深入学习 ​

本章内容必须掌握。pom.xml 和 Maven 生命周期是看懂任何 Java 项目的前提。

建议:

  1. 实际创建一个 Maven 项目并跑通
  2. 配置阿里云镜像加速依赖下载
  3. 学会用 mvn clean package 和 java -jar 启动项目

准备好了吗? 继续阅读 第五章:Spring Boot 入门 →