从零散编译到一键打包:Maven如何重塑Java构建流程

4876 Views

本文相关代码:https://gitea.notnull.com.cn/yangw/mvn4java

灵魂拷问:不使用IDE工具,你能运行打包一个复杂的Java项目吗?

## 前置知识点

> 开始之前,先来了解两个JDK自带的工具,`javac`和`java`,还有`jar`压缩包

### javac

`javac` 是 **Java Compiler** 的缩写,它是 Java 开发工具包(JDK)中自带的一款**命令行工具**,其核心功能是**将人类可读的 Java 源代码(`.java`文件)编译成 Java 虚拟机(JVM)可以执行的字节码(`.class`文件)** 。

`javac`常用重要选项(Options)

| 选项 | 说明 | 示例 || ----------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ || `-d ` | **指定输出目录**。编译生成的 `.class` 文件会被放置到这个指定的目录下。如果目录不存在,`javac` 会自动创建。 | `javac -d out src/HelloWorld.java` (假设 src 目录下有 HelloWorld.java) || `-sourcepath ` | 指定查找源文件(`.java`)的路径。当你的代码中引用了其他包中的类时,`javac` 会在这些路径下寻找对应的 `.java` 文件。 | `javac -sourcepath src -d out src/com/example/Main.java` || `-classpath ` 或 `-cp ` | **指定类路径**。用于查找编译时所依赖的外部 `.class` 文件或 JAR 包。如果你的代码使用了第三方库(如 Jackson 等),就必须用此选项告诉 `javac` 去哪里找这些库。 | `javac -cp lib/xxx.jar:. Main.java` (`.` 代表当前目录) |

### java

`java` **命令行工具**是 Java Runtime Environment (JRE) 的核心,它的作用是**启动 Java 虚拟机(JVM)并执行指定的 Java 应用程序(即 `.class` 文件中的字节码)** 。

`java`常用重要选项(Options)

| 选项 | 说明 || ----------------------------------- | ------------------------------------------------------------ || `-cp <路径>` 或 `-classpath <路径>` | **(最重要)指定类路径**。告诉 JVM 去哪里查找用户定义的类和第三方库(JAR 文件)。 || `-version` | **执行一个可执行的 JAR 文件**。JVM 会从该 JAR 文件的 `META-INF/MANIFEST.MF` 文件中查找 `Main-Class` 属性来确定主类。 || `-jar <文件名>` | 显示 Java 运行时环境的版本信息并退出。 |> java文件被javac编译器编译成了class文件后,java工具调用JVM执行class文件,那是否可以把其他语言也编译成class文件给JVM执行呢?>> 当然可以,在java之外,有*Jython*、*JRuby*,还有*Kotlin*、*Scala*等语言,它们都实现了自己的“javac“,可以编译成符合JVM规范的class文件,然后同样可以将这些语言编译出来的class文件运行于JVM上### jar

[JAR File Specification](https://docs.oracle.com/en/java/javase/25/docs/specs/jar/jar.html)

**JAR 包(Java Archive)** 是 Java 平台中用于打包、分发和部署 Java 程序或库的**归档文件格式**。它的本质是一个压缩文件(基于 ZIP 格式),但扩展名为 `.jar`,专门用于封装 Java **类文件**(`.class`)、资源文件(如配置文件、图片、音频等)、元数据(如清单文件 `MANIFEST.MF`)以及依赖的其他 JAR 包。

一个典型的`jar`包结构通常如下所示,包括**META-INF目录**和**类文件和资源文件**,其实相对于普通压缩包而言,区别就是多了一个描述性的清单文件`MANIFEST.MF`。

```app.jar├── META-INF/ # 元数据目录(必须)│ └── MANIFEST.MF # 清单文件(记录包信息,如主类、依赖等)├── com/ # Java 类文件的包结构(对应源码中的 package)│ └── example/│ └── Main.class # 编译后的字节码文件├── config.properties # 配置文件(应用运行时需要的参数)└── logo.png # 静态资源(如应用图标)```

清单文件(`META-INF/MANIFEST.MF`)是 JAR 包的“说明书”,用于描述包的基本信息和运行规则。一个简单`MANIFEST.MF`的示例如下:

```Manifest-Version: 1.0 # 清单文件版本(如 1.0)。Created-By: 17.0.2 (Oracle Corporation) # 生成该 JAR 的工具(如 Apache Maven 3.8.0)。Main-Class: com.example.Main # 可执行 JAR 包的关键,指定程序入口类(含 main方法的类),例如 com.example.Main。Class-Path: lib/utils.jar lib/logging.jar # 声明当前 JAR 依赖的其他 JAR 包路径(相对或绝对),运行时 JVM 会自动加载这些依赖。```

`jar`包一般分为两类,一类是可执行`jar`包(应用程序包),另一类是普通`jar`包(类库包)。

我们导入的`Junit`、`Lombok`这些第三方`jar`包属于普通`jar`包,它们的MANIFEST.MF文件中不会描述Main-Class属性,因为它们不需要运行,只是作为一种类库导入给其他程序使用。而我们自己开发的应用程序,打包后通常属于可执行`jar`,需要通过Main-Class属性执行程序运行的入口类,然后可以使用`java -jar xxx.jar`执行该jar包。

> 为什么jar包存放class文件而不是java文件?>> 1. JVM 只能执行字节码,无法直接运行源代码> 2. 字节码是跨平台的“中间语言” (实际上Java项目有时引入的第三方Jar包可能并不是Java编写的)> 3. 字节码是二进制文件,体积更小,适合分发和存储> 4. 简化部署,无需用户手动编译

### classpath到底是什么?

`classpath`是JVM用到的一个环境变量,它用来指示JVM如何搜索`class`。因为Java是编译型语言,源码文件是`.java`,而编译后的`.class`文件才是真正可以被JVM执行的字节码。因此,JVM需要知道,如果要加载一个`cn.com.notnull.Main`的类,应该去哪搜索对应的`Main.class`文件。所以`classpath`就是一组目录的集合,告诉JVM去这些路径下找class文件。如果没有设置,默认的类路径就是当前目录。**但如果设置了类路径,默认值就被覆盖了,所以如果想保留当前目录为类路径,需要同时将`.`加入**,有点像默认构造函数的感觉。

java核心类需要指定classpath加载吗?比如String,ArrayList这些类。不需要,JVM自动会去默认指定位置加载核心类。

## 没有Maven的时候

### 阶段一:单文件

> 从最简单的单文件Java项目开始,抛开IDE工具

#### 编译和运行

创建一个Java项目,位于~/workspace/mvn4java/project-1,它只有一个`Main.java`文件。

```java// package cn.com.notnull.mvn4java;

public class Main { public static void main(String[] args) { System.out.println("hello maven"); } } ```

命令行编译并运行它,编译后在Main.java所在目录将会看到Main.class字节码文件

```shellcd ~/workspace/mvn4java/project-1javac Main.java```

现在目录结构如下

```.└── project-1 ├── Main.class └── Main.java```

因为使用`javac`时没有指定`-d`输出目录,所以`.class`文件默认生成在了`.java`文件所在的目录。

现在使用`java Main`即可运行Main:

```shelljava Mainhello maven```

**进一步理解package和classpath**

> projecte-1项目目前没有创建packge,Main.java的packge声明是注释掉的,你可以想一下,放开这条注释,但是**没有把java文件放在包路径对应文件夹内**,此时javac还能正常编译吗?如果能编译,那么java能运行吗?

如果现在放开`Main.java`中package的注释,使用`javac Main.java`编译是成功的,并且生成的`class`文件仍然在当前目录下。

但是直接运行`java Main`会报错,那运行时指定类的全限定类名呢(`java cn.com.notnull.mvn4java.Main`),结果是一样的,都会报错找不到指定的类,也就是说JVM无法找到Main类:

```Error: Could not find or load main class Main```

要知道报错原因,需要知道JVM是按照什么规则去寻找class文件的,实际上JVM会通过Classpath和**全限定类名**(`包名.类名`如 `cn.com.notnull.mvn4java.Main`)查找类文件,按以下方式解析查找的路径:

```shell# classpath + 全限定类名映射的物理路径# 如下例子cn.com.notnull.mvn4java.Main --解析为--→ [classpath/cn/com/notnull/mvn4java/Main.class] # classpath将被替换为实际物理路径```

`java`工具运行不指定classpath选项时**默认的classpath是`.`,**也就是当前目录。

再回过来看之前的报错问题:

- Main.java未指定包名时全限定类名就是`Main`,JVM查找Main时就会在`.`(也就是当前目录)下查找Main.class,那自然是找到了,`java Main`运行成功。

- Main.java指定包名`cn.com.notnull.mvn4java`后,类的全限定名称就不再是`Main`,而是`cn.com.notnull.mvn4java.Main`,JVM查找Main.class类时,按照上面说的方式(classpath+全限定类名)解析路径,就会在`./cn/com/notnull/mvn4java`下查找(classpath默认是`.`)。但是Main.class编译后在了`.`下,JVM自然就无法找到Main类。**那我们创建一个包路径`cn/com/notnull/mvn4java`,并将`Main.class`放进去,这个时候执行`java cn.com.notnull.mvn4java.Main`就不会报错了。**

**对于声明包名后无法运行的情况你可能会有一个疑问**:虽然Main.java声明了包名,但Main.class还是编译到了`.`下,java默认的classpath也是`.`,那JVM不就看到Main.class文件了吗?为什么无法直接执行`java Main`或`java cn.com.notnull.mvn4java.Main`呢?

**因为对于JVM来说全限定类名一致才认为是同一个类**

- 如果你执行`java Main`,JVM会在`.`寻找一个全限定类名是**Main**的类,虽然找到了一个Main.class,但这个class文件描述的类的全限定类名是`cn.com.notnull.mvn4java.Main`,并不匹配传入的`Main`。- 如果你执行`java cn.com.notnull.mvn4java.Main`,那么JVM会去`./cn/com/notnull/mvn4java`查找Main.class类而不是`.`路径,直接就找不到了。

**总之就是JVM通过classpath路径和全限定类名定位一个类,并且只有全限定类名一致的类才能被认为是同一个类**

> 使用Java时一定要注意将包名跟物理路径一一对应,当然这个工作IDE已经帮我们完成了。

现在还有一个问题,我们将源文件放在了包路径下,但是编译后java源文件跟class字节码文件混在了一起,造成项目结构混乱。所以应该专门创建一个目录用于存放编译后的class文件,但是需要自己创建包路径的目录,太麻烦。

`javac`有一个选项`-d`,`-d`指定了生成class文件的根目录,并且会根据class的包路径创建子目录,这样就省去了我们手动创建编译文件包路径的操作了。

示例如下,指定class文件输出到当前目录的output下:

```shellmkdir ./output # 创建output目录javac -d ./output cn/com/notnull/mvn4java/Main.java # 编译tree # 查看目录结构.└── cn └── com └── notnull └── mvn4java └── Main.class└── output └── cn └── com └── notnull └── mvn4java └── Main.class```

可以看到,`javac`帮我们在output目录自动创建了包路径对应的目录。那现在可以直接使用`java`运行了吗?

现在执行`java cn.com.notnull.mvn4java.Main`又报错了,相信你已经知道原因了。执行`java`时没有指定classpath,所以默认classpath是`.`当前目录,JVM会在`./cn/com/notnull/mvn4java`目录下查找class,但实际上我们生成的class在output(`./output/cn/com/notnull/mvn4java`)下。

那运行时指定classpath为output,告诉JVM去output下找class就行了,将命令换成`java -cp ./output cn.com.notnull.mvn4java.Main`即可,注意运行时需要指定类的**全限定类名**。

```shell# 当前路径在 ~/workspace/mvn4java/project-1java -cp ./output cn.com.notnull.mvn4java.Mainhello maven```

此外,还可以直接进入目录`~/workspace/mvn4java/project-1/output`,然后继续执行`java cn.com.notnull.mvn4java.Main`,因为classpath默认是`.`,进入output执行命令自然也能找到`./cn/com/notnull/mvn4java/Main.class`

```shellcd ~/workspace/mvn4java/project-1/outputjava cn.com.notnull.mvn4java.Mainhello maven```

#### 打包

现在完成了一个简单项目,那么怎么交付它?直接把源代码发送给别人运行吗?当然不是,这里就需要用之前提到的jar包了。我们的程序是应用程序,所以打包时应该打可执行jar包。具体步骤如下:1. 编译

```shell cd ~/workspace/mvn4java/project-1 javac -d ./output Main.java ```

2. 在项目根目录创建清单文件 (MANIFEST.MF)

``` Manifest-Version: 1.0 Main-Class: cn.com.notnull.mvn4java.Main Created-By: Manual-Packaging ```

3. 使用JDK提供的jar工具打包

```shell jar cvfm Main.jar MANIFEST.MF -C output/ . # 注意最后有一个. #####输出##### added manifest adding: cn/(in = 0) (out= 0)(stored 0%) adding: cn/com/(in = 0) (out= 0)(stored 0%) adding: cn/com/notnull/(in = 0) (out= 0)(stored 0%) adding: cn/com/notnull/mvn4java/(in = 0) (out= 0)(stored 0%) adding: cn/com/notnull/mvn4java/Main.class(in = 437) (out= 301)(deflated 31%) ```

`c`:创建新 JAR;`v`:显示详细过程;`f`:指定 JAR 文件名;`m`:指定清单文件;`-C out/ .`:切换到 `out/`目录,将当前目录所有文件(含子目录)加入 JAR。

现在目录结构如下:

```.├── Main.jar├── Main.java├── MANIFEST.MF└── output └── cn └── com └── notnull └── mvn4java └── Main.class```

执行`java -jar Main.jar`,查看结果运行正常:

```shelljava -jar Main.jarhello maven```

### 阶段二:多文件

> 我们不再满足于只编写一个Java文件的项目了,需要编写多个Java文件,并且还会将不同的功能的Java文件放置在不同的包下

#### 编译和运行

创建一个Java项目,位于`~/workspace/mvn4java/project-2`,这次我们按照规范创建包和类,创建`cn.com.notnull.mvn4java`和`cn.com.notnull.mvn4java.utils`,并在主包下创建`Main.java`,在`utils`包下创建`Sort.Java`,`Sort.Java`是一个用于排序的类,我们需要在`Main.java`中引入它完成排序功能。

项目结构如下:

```.└── cn └── com └── notnull ├── Main.java └── utils └── Sort.java```

Main.java

```javapackage cn.com.notnull.mvn4java;import cn.com.notnull.mvn4java.utils.Sort;// Main.javapublic class Main { public static void main(String[] args) { int[] numbers = {5, 2, 9, 1, 5, 6}; System.out.println("排序前:"); for (int num : numbers) { System.out.print(num + " "); }

// 调用 Sort 类的静态方法进行排序 Sort.bubbleSort(numbers);

System.out.println("\n排序后:"); for (int num : numbers) { System.out.print(num + " "); } }}```

Sort.java

```javapackage cn.com.notnull.mvn4java.utils;// Sort.javapublic class Sort { public static void bubbleSort(int[] array) { int n = array.length; for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n - i - 1; j++) { if (array[j] > array[j + 1]) { // 交换元素 int temp = array[j]; array[j] = array[j + 1]; array[j + 1] = temp; } } } }}```

现在完成了项目的创建,那么这时候应该怎么编译和运行?

首先创建一个目录用于存放所有编译后的class文件,便于管理;然后使用`javac`添加`-d`参数指定输出目录,`javac`会自动创建子目录。但是还有一些问题,现在java文件散落在不同的包(目录)中,`javac`编译时怎么能够一次性将它们全部编译呢?又或者只编译某一个包的类?类之间有依赖关系怎么处理编译的先后顺序?

多文件编译方式:

1. 指定要编译的主类,`javac`会自动找到主类所依赖的java源文件编译成class文件2. **遍历**所有java源文件并传递给javac编译,javac会智能处理类之间的依赖关系3. **列表文件**的方法,将要编译的java文件列表写到一个文本文件里,让javac读取4. 一个一个源文件编译,不推荐,需要手动处理依赖关系,被依赖的类要先被编译

以下使用指定要编译的主类的方式进行编译。

`javac`工具的常用选项有一个`-sourcepath `的选项可以指定查找源文件(`.java`)的路径。比如我们将包都放在了当前目录目录下,那么就指定`sourcepath`为`.`即可。如果一次性将所有要编译的文件传递给`javac`,那么可以不指定`-sourcepath`;如果只传递主类,则需要指定`-sourcepath`告诉`javac`从哪个路径寻找主类所依赖的类。

这里使用指定主类方法编译,指定sourcepath为当前目录,javac就会在当前目录下开始查找依赖的类,最终编译命令就是`javac -sourcepath . -d ./target cn/com/notnull/mvn4java/Main.java`

```shellcd ~/workspace/mvn4java/project-2mkdir target # 这次创建target目录存放编译后的文件javac -sourcepath . -d target cn/com/notnull/mvn4java/Main.java # 注意sourcepath指定了.目录,也就是当前目录```

```shell# 如果是第二种方式遍历编译,使用find命令找到当前目录下所有Java文件,这时可以不指定sourcepath javac -d target $(find . -name "*.java")```

编译完成后目录结构如下:

```.├── cn│ └── com│ └── notnull│ └── mvn4java| ├── Main.java │ └── utils│ └── Sort.java└── target └── cn └── com └── notnull └── mvn4java ├── Main.java └── utils └── Sort.class```

那么现在就可以运行Main,只需要指定target目录作为classpath即可,`java -cp ./target/ cn.com.notnull.Main`

```shelljava -cp ./target/ cn.com.notnull.Main排序前:5 2 9 1 5 6排序后:1 2 5 5 6 9 ```

**总结**:运行Java时,classpath的值很关键,classpath告诉了JVM到哪里找类。默认情况下classpath是当前目录,如果设置了其他值将覆盖默认值,如果既需要其他classpath又需要当前目录,可以同时指定多个目录,classpath支持多个路径。

#### 一点优化

现在项目根路径下直接就是包路径,如果在项目中使用了其他多个包路径,看起来就会很混乱,为了更好的项目管理,决定在根目录下创建一个`src`目录用于存放`java`源文件,所有的包都在这个目录下创建。现在项目结构如下:

```.├── src│ └── cn│ └── com│ └── notnull│ └── mvn4java│ ├── Main.java│ └── utils│ └── Sort.java└── target```

修改项目结构后,因为包都放到了src目录下,编译命令也会有所变化,需要通过`sourcepath`指定`src`作为源文件路径`javac -sourcepath src -d ./target src/cn/com/notnull/mvn4java/Main.java`,但是运行`java`命令跟之前还是一样。

#### 打包

这次的项目变得复杂了一点,有了多个文件,现在使用jar包交付项目该怎么打包呢?

因为没有引入项目之外的依赖,所以编译完成后打包方式跟之前一样。都是创建一个MANIFEST.MF,然后执行`jar cvfm Main.jar MANIFEST.MF -C target .`即可。

### 阶段三:依赖三方Jar

> 更进一步,我不仅需要多个包和文件,我还需要拿别人写好的Java库(jar)来用

#### 编译和运行

创建一个Java项目,位于`~/workspace/mvn4java/project-3`,这次不仅在src下创建一个`java`目录存放java源文件;此外还需要创建一个`lib`目录用于存放jar包,一个`target`存放class文件(是不是很像maven项目的结构了)。

实现一个简单服务,查询用户数据并返回json格式,Java对象转json选择使用第三方Jackson库。

**Jackson 核心模块**

- Jackson Core:Jackson 的核心模块,提供了基本的 JSON 解析和生成功能。- Jackson Annotations:包含各种注解,用于控制序列化和反序列化过程;只包含了注解,没有实际功能,需搭配core模块- Jackson Databind:提供数据绑定功能,可以在 Java 对象和 JSON 之间进行转换;该模块依赖于core和annotation模块

使用databind模块可以获得所有功能,简化操作,databind依赖core和annotation,那么还需要将这两个模块同时导入项目。

下载**相同版本**的三个Jar放入lib目录,最终项目结构如下:

```.├── lib│ ├── jackson-annotations-2.20.jar│ ├── jackson-core-2.20.0.jar│ ├── jackson-databind-2.20.0.jar├── src│ ├── java│ └── cn│ └── com│ └── notnull│ └── mvn4java│ ├── model│ │ └── User.java│ ├── Server.java│ └── service│ └── UserService.java│ └── target```

User.java

```javapublic class User {

private Long id; @JsonProperty(value = "username") private String name; private Integer age; public static List createUserList() { List list = new ArrayList<>(); for (int i = 0; i < 3; i++) { list.add(new User((long) i, "张三" + i + 1 + "号", 18 + i)); } return list; }}```

UserService.java

```javapublic class UserService { public List getUserList() { return User.createUserList(); }}```

Server.java

```javapublic class Server {

public static void main(String[] args) throws JsonProcessingException { UserService service = new UserService(); List userList = service.getUserList();

// Jackson使用 ObjectMapper mapper = new ObjectMapper(); // 对象转json字符串 String json = mapper.writeValueAsString(userList); System.out.println("json = " + json); }}```

项目编写阶段完成了,接下来要怎么进行编译和运行呢?

##### 编译

程序依赖了外部类,那编译的时候告诉`javac`去哪找依赖的类就好了,之前的阶段中有一个`javac`选项一直没有使用,那就是`-cp/classpath`,该选项跟`java -cp`一样,都是用于查找依赖的外部 `.class` 文件或 JAR 包。多个classpath依赖使用`:`分隔(windows下使用`;`)。最终编译命令如下:

```shellcd ~/workspace/mvn4java/project-3javac -d target/ -sourcepath src/java/ -cp "lib/jackson-annotations-2.20.jar:lib/jackson-core-2.20.0.jar:lib/jackson-databind-2.20.0.jar" src/java/cn/com/notnull/mvn4java/Server.java# 使用通配符简化成javac -d target/ -sourcepath src/java/ -cp "lib/*" src/java/cn/com/notnull/mvn4java/Server.java```

编译成功,target下生成了包路径:

```└── target └── cn └── com └── notnull └── mvn4java ├── model │ └── User.class ├── Server.class └── service └── UserService.class```

##### 运行

不仅编译时需要指定外部依赖的classpath,运行时同样需要指定外部依赖classpath,同时不要忘了将项目自身的target目录加入classpath。最终运行命令如下:

```shelljava -cp "target:lib/jackson-annotations-2.20.jar:lib/jackson-core-2.20.0.jar:lib/jackson-databind-2.20.0.jar" cn.com.notnull.mvn4java.Server# 使用通配符简化成java -cp "target:lib/*" cn.com.notnull.mvn4java.Server# 输出json = [{"id":0,"age":18,"username":"张三01号"},{"id":1,"age":19,"username":"张三11号"},{"id":2,"age":20,"username":"张三21号"}]```

#### 打包

这次的项目变得更复杂了,不仅有了多个文件,还引入了第三方的jar包,那现在使用jar包交付项目该怎么打包呢?

使用外部依赖后手动打包方式也分为两种

- 普通打包(依赖外置),只打包本项目编译后的class文件,并在清单文件中使用Class-Path设置依赖包相对路径- 打包为胖 JAR(依赖内置),直接将第三方 JAR 的字节码解压到自己的target目录,再打包

###### 普通打包

跟之前阶段的打包步骤一样,唯一不同就是需要在`MANIFEST.MF`中设置Class-Path,这里不能使用通配符,依赖之间使用空格分隔。

```Manifest-Version: 1.0Main-Class: cn.com.notnull.mvn4java.ServerCreated-By: Manual-PackagingClass-Path: lib/jackson-annotations-2.20.jar lib/jackson-core-2.20.0.jar lib/jackson-databind-2.20.0.jar```

```shelljar -cvfm Server.jar MANIFEST.MF -C target/ .```

执行jar命令后会生成一个Server.jar,执行`java -jar Server.jar`正常运行

```shelljava -jar Server.jarjson = [{"id":0,"age":18,"username":"张三01号"},{"id":1,"age":19,"username":"张三11号"},{"id":2,"age":20,"username":"张三21号"}]```

然后你把`Server.jar`放到服务器上的`/server`路径准备部署,发现`java -jar Server.jar`运行错误,并且报错`NoClassDefFoundError`。

```shell# 假装jar包传输到服务器server目录cd /serverjava -jar Server.jarError: A JNI error has occurred, please check your installation and try againException in thread "main" java.lang.NoClassDefFoundError: com/fasterxml/jackson/core/JsonProcessingException at java.lang.Class.getDeclaredMethods0(Native Method) at java.lang.Class.privateGetDeclaredMethods(Class.java:2701) at java.lang.Class.privateGetMethodRecursive(Class.java:3048) at java.lang.Class.getMethod0(Class.java:3018) at java.lang.Class.getMethod(Class.java:1784) at sun.launcher.LauncherHelper.validateMainClass(LauncherHelper.java:670) at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:652)Caused by: java.lang.ClassNotFoundException: com.fasterxml.jackson.core.JsonProcessingException at java.net.URLClassLoader.findClass(URLClassLoader.java:387) at java.lang.ClassLoader.loadClass(ClassLoader.java:418) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352) at java.lang.ClassLoader.loadClass(ClassLoader.java:351) ... 7 more```

原因就是打包的`Server.Jar`包并不包含Jackson的类,只是在`MANIFEST.MF`中设置了依赖包的路径,那就需要将外部依赖Jar包放置在Class-Path设置的相对路径。当前`MANIFEST.MF`配置的**Class-Path: lib/jackson-annotations-2.20.jar lib/jackson-core-2.20.0.jar lib/jackson-databind-2.20.0.jar**。那么依赖包的相对路径就是Jar包同级的lib。将之前项目的lib目录复制到服务器的/server目录,保持lib跟Server.jar同级。此时执行`java -jar Server.jar`就可以正常运行。

```server├── lib│ ├── jackson-annotations-2.20.jar│ ├── jackson-core-2.20.0.jar│ ├── jackson-databind-2.20.0.jar└── Server.jar```

```shelljava -jar Server.jarjson = [{"id":0,"age":18,"username":"张三01号"},{"id":1,"age":19,"username":"张三11号"},{"id":2,"age":20,"username":"张三21号"}]```

###### Fat Jar打包

FatJa打包的区别就是打包的时候将依赖的Jar中的class解压到自己的class编译输出目录(target)。不再详细描述,这种打包方式本质就是将外部依赖放在了自己的Jar包中,最常见的就是SpringBoot打包后的Jar包,可以直接运行。

## 引入Maven

> 本文不再介绍maven的概念和使用

**maven项目结构规范**

| 文件夹结构 | 描述 || :------------------ | :------------------------------------------------- || src/main/java | java 代码文件在包结构下。 || src/test/java | 测试代码文件在包结构下。 || target/classes | src/main/java下的java文件编译后的目录 || target/test-classes | src/test/java 测试包下的java文件编译后的目录 || src/main/resources | classpath 资源文件,这里的文件编译后会被放入target || src/test/resources | 放置测试用到的 classpath 资源文件 |

**你可能会有一个疑问**,我们定义的pom.xml文件并没有设置这些目录,maven在什么地方默认配置了这些目录?答案就是我们编写的pom.xml文件并不是maven最终使用的pom文件,maven自身还有一个超级pom,自定义的pom.xml文件将会继承这个超级pom。这个超级pom路径在:`maven安装目录\lib\maven-model-builder-3.6.3.jar`包中的`org\apache\maven\model\pom-4.0.0.xml`。在这个pom文件中有一段内容如下:

```xml${project.basedir}/target${project.build.directory}/classes${project.artifactId}-${project.version}${project.build.directory}/test-classes${project.basedir}/src/main/java${project.basedir}/src/main/scripts${project.basedir}/src/test/java```

### 阶段四:maven构建

现在来创建一个maven项目`project-4`,使用maven提供的**archetype**模板

```shellmvn archetype:generate "-DgroupId=cn.com.notnull.mvn4java" "-DartifactId=project-4" "-DarchetypeArtifactId=maven-archetype-quickstart" "-DinteractiveMode=false"```

`resources`目录需要手动创建,`target`目录编译会自动创建。

将`project-3`项目的代码文件拿过来,并编写pom文件,引入jackson-databind坐标。

pom.xml

```xml

4.0.0 cn.com.notnull.mvn4java project-4 jar 1.0-SNAPSHOT project-4

com.fasterxml.jackson.core jackson-databind 2.20.0

```

只引入了`jackson-databind`包,因为`jackson-databind`依赖`jackson-core`和`jackson-annotations`。maven会自动解析这些依赖并下载。

执行`mvn dependency:tree`可以查看项目的依赖树,可以看到core和annotations已经被解析出来了。

```[INFO] cn.com.notnull.mvn4java:project-4:jar:1.0-SNAPSHOT[INFO] \- com.fasterxml.jackson.core:jackson-databind:jar:2.20.0:compile[INFO] +- com.fasterxml.jackson.core:jackson-annotations:jar:2.20:compile[INFO] \- com.fasterxml.jackson.core:jackson-core:jar:2.20.0:compile```

第一次运行一些mvn命令时发现mvn会先执行一些下载操作,这是因为maven的所有操作和功能都是由插件来实现的,执行命令发现插件不存在就会下载对应的插件。

#### 编译

使用`mvn compile`命令即可编译整个maven项目,compile过程会自动触发依赖下载机制。编译后发现多了个`target`目录:

```└── target ├── classes │ └── cn │ └── com │ └── notnull │ └── mvn4java │ ├── App.class │ ├── model │ │ └── User.class │ ├── Server.class │ └── service │ └── UserService.class```

对比`javac`编译,我们没有指定依赖的classpath,也没有指定输出的target目录,maven都帮我们完成了。

#### 运行

可以使用maven的exec插件直接运行Java类的main方法。现在使用`mvn exec:java -Dexec.mainClass=cn.com.notnull.mvn4java.Server"`运行,运行成功

```jsonjson = [{"id":0,"age":18,"username":"张三01号"},{"id":1,"age":19,"username":"张三11号"},{"id":2,"age":20,"username":"张三21号"}]```

对比`java`运行,照样不需要指定各个依赖的classpath,只需要指定我们要运行的类即可。

#### 打包

**使用 `maven-jar-plugin`(生成不可直接执行的标准 JAR)**

maven功能是基于插件的,maven有很多内置插件,而默认的打包插件就是`maven-jar-plugin`,`maven-jar-plugin`的职责很单一:将项目编译好的 class 文件(位于 target/classes)和资源文件(位于 src/main/resources)打包成一个标准的、不可直接执行的 JAR 文件,这个 JAR 文件只包含你自己的代码,不包含任何第三方依赖,跟之前的普通打包结果是一样。

给`maven-jar-plugin`插件配置主类

plugin:

```xml org.apache.maven.plugins maven-jar-plugin true lib/ cn.com.notnull.mvn4java.Server ```

执行`mvn package`,查看打包后的MANIFEST.MF,插件帮我们自动生成了MANIFEST.MF文件,并设置了所有的依赖路径,不需要手动编写了。

```Manifest-Version: 1.0Class-Path: lib/jackson-databind-2.20.0.jar lib/jackson-annotations-2. 20.jar lib/jackson-core-2.20.0.jar # 自动生成了依赖路径Build-Jdk-Spec: 1.8Created-By: Maven JAR Plugin 3.4.1Main-Class: cn.com.notnull.mvn4java.Server```

那么这个jar包怎么运行你也知道了,需要将依赖包放入同级的lib目录中。

**到此为止了吗?**

我想打包一个直接可以运行的Jar包如何操作?

除了maven-jar-plugin,maven的世界里还有很多其他的打包插件可以使用。

1. maven-shade-plugin: 创建通用的、可执行的 Fat JAR。2. maven-assembly-plugin: 高度可定制化地创建分发包,如 ZIP, TAR.GZ, Dir 等。适合复杂项目3. spring-boot-maven-plugin: 专为 Spring Boot 项目打造,创建可执行的 Fat JAR (或 Fat WAR)。

接下来选择maven-shade-plugin进行打包。

**maven-shade-plugin核心用途**

1. **创建可执行 Fat JAR**:这是最主要的功能。它将所有依赖的 `.class`文件打包进一个最终的 JAR 中,使得 `java -jar xxx.jar`可以直接运行。2. **处理资源转换 (Transformations)**:合并或转换在依赖中可能存在的同名文件,最经典的例子是合并 `META-INF/services/`目录下的文件,这对于 Java SPI (Service Provider Interface) 机制至关重要。3. **重命名类 (Relocating Classes)**:解决依赖冲突的神器。如果两个不同的依赖库包含了相同包名的类,可以通过重命名其中一个库的包路径来避免冲突。4. **创建无依赖的发行版**:最终产物是一个独立的文件,便于分发和部署。

pom.xml设置

```xml org.apache.maven.plugins maven-shade-plugin 3.5.1 package shade cn.com.notnull.mvn4java.Server ```

执行`mvn clean package`进行打包操作,maven会自动下载`maven-shade-plugin`插件,并执行清理、编译、打包等操作。执行完毕后在target目录下将会看到`maven-shade-plugin`插件打包出来的Jar,有两个Jar,一个是不包含依赖的原始jar,一个是包含依赖的FatJar。

```jsonproject-4-1.0-SNAPSHOT.jaroriginal-project-4-1.0-SNAPSHOT.jar```

在Jar包目录执行FatJar: `jar -jar project-4-1.0-SNAPSHOT.jar`

```jsonjson = [{"id":0,"age":18,"username":"张三01号"},{"id":1,"age":19,"username":"张三11号"},{"id":2,"age":20,"username":"张三21号"}]```

之前并没有展示手动的方式打包FatJar,因为比较麻烦,甚至容易出现依赖冲突,但是使用maven后,这些问题都被插件解决了,只需要一点简单的插件配置,然后执行mvn命令就可以完成复杂的打包操作。

## 总结

在没有Maven的时代,Java项目构建全靠手动:用`javac`编译要管依赖路径,用`java`运行要配classpath,打包还要处理第三方jar包的存放问题,项目稍微复杂点就容易报错。

Maven的出现彻底改变了这一局面——它像一个“智能管家”,帮开发者自动完成了:

1. **依赖管理**:不用手动下载jar包,通过坐标一键引入并解决依赖冲突;2. **标准化流程**:规范了项目结构(如代码放`src/main/java`、资源放`src/main/resources`),统一了编译、测试、打包的步骤;3. **一键构建**:只需敲`mvn compile`(编译)、`mvn exec:java`(运行)、`mvn package`(打包),复杂操作(如生成包含所有依赖的可执行Fat JAR)全由插件自动处理;4. **效率提升**:告别手动管理jar包路径、依赖版本和构建顺序的繁琐,让开发者专注写业务代码。

简言之,Maven把原本需要“手敲几十行命令+反复调试”的Java构建流程,简化成了“配置一次+一键执行”的高效体验。除此之外,Maven还有更多强大功能:

- 多模块管理:支持将大型项目拆分为多个子模块(如核心模块、Web模块、工具模块),通过父子POM统一管理依赖和构建逻辑,避免重复配置;

- 生命周期与插件体系:内置清晰的构建生命周期(如clean、compile、test、package、install、deploy),通过丰富的插件(如生成项目文档的maven-javadoc-plugin、代码质量检查的maven-checkstyle-plugin)扩展功能;

- 仓库管理:自动从本地仓库或远程仓库(如Maven中央仓库)下载依赖,支持私有仓库部署,确保团队协作时依赖版本一致;

- 跨环境一致性:通过POM文件锁定依赖版本和构建配置,保证开发、测试、生产环境构建结果完全一致;

- 项目信息管理:在POM中定义项目描述、开发者信息、许可证等元数据,便于团队协作和项目维护。

Maven不仅是“构建工具”,更是Java项目的“全流程管理专家”,从依赖到部署、从单人开发到团队协作,全方位提升开发效率与可靠性。

## 扩展

#### SpringBoot为什么可以直接运行?

《冲顶大会》几点开始答题 每日答题时间介绍
風車 (2011) Full online with English subtitle for free – iQIYI