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

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

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

前置知识点

开始之前,先来了解两个JDK自带的工具,javacjava,还有jar压缩包

javac

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

javac常用重要选项(Options)

选项 说明 示例
-d <directory> 指定输出目录 。编译生成的 .class 文件会被放置到这个指定的目录下。如果目录不存在,javac 会自动创建。 javac -d out src/HelloWorld.java (假设 src 目录下有 HelloWorld.java)
-sourcepath <path> 指定查找源文件(.java)的路径。当你的代码中引用了其他包中的类时,javac 会在这些路径下寻找对应的 .java 文件。 javac -sourcepath src -d out src/com/example/Main.java
-classpath <path>-cp <path> 指定类路径 。用于查找编译时所依赖的外部 .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之外,有JythonJRuby ,还有KotlinScala等语言,它们都实现了自己的"javac",可以编译成符合JVM规范的class文件,然后同样可以将这些语言编译出来的class文件运行于JVM上

jar

JAR File Specification

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

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

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

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

yaml 复制代码
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包(类库包)。

我们导入的JunitLombok这些第三方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. 简化部署,无需用户手动编译

注意事项

  • 依赖冲突 :若多个 JAR 包包含同名类(如不同版本的库),可能导致 NoClassDefFoundError或方法冲突
  • 可执行 JAR 的限制 :默认情况下,可执行 JAR 无法嵌套其他 JAR 包(即 Class-Path只能指向外部 JAR)。若需打包所有依赖,可使用 Fat JAR (如 Spring Boot 的 spring-boot-maven-plugin生成的包)或 Shaded JAR(重命名冲突类)。
  • 跨平台性:JAR 包本身是平台无关的(因 Java 字节码跨平台),但运行需安装对应版本的 JVM。

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字节码文件

shell 复制代码
cd ~/workspace/mvn4java/project-1
javac Main.java

现在目录结构如下

css 复制代码
.
└── project-1
    ├── Main.class
    └── Main.java

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

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

shell 复制代码
java Main
hello 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类:

arduino 复制代码
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 Mainjava 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下:

shell 复制代码
mkdir ./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-1
java -cp ./output cn.com.notnull.mvn4java.Main
hello maven

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

shell 复制代码
cd ~/workspace/mvn4java/project-1/output
java cn.com.notnull.mvn4java.Main
hello maven
打包

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

  1. 编译

    shell 复制代码
    cd ~/workspace/mvn4java/project-1
    javac -d ./output Main.java
  2. 在项目根目录创建清单文件 (MANIFEST.MF)

    makefile 复制代码
    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。

现在目录结构如下:

css 复制代码
.
├── Main.jar
├── Main.java
├── MANIFEST.MF
└── output
    └── cn
        └── com
            └── notnull
                └── mvn4java
                    └── Main.class

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

shell 复制代码
java -jar Main.jar
hello maven

阶段二:多文件

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

编译和运行

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

项目结构如下:

csharp 复制代码
.
└── cn
    └── com
        └── notnull
            ├── Main.java
            └── utils
                └── Sort.java

Main.java

java 复制代码
package cn.com.notnull.mvn4java;
import cn.com.notnull.mvn4java.utils.Sort;
// Main.java
public 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

java 复制代码
package cn.com.notnull.mvn4java.utils;
// Sort.java
public 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

shell 复制代码
cd ~/workspace/mvn4java/project-2
mkdir target  # 这次创建target目录存放编译后的文件
javac -sourcepath . -d target cn/com/notnull/mvn4java/Main.java # 注意sourcepath指定了.目录,也就是当前目录
shell 复制代码
# 如果是第二种方式遍历编译,使用find命令找到当前目录下所有Java文件,这时可以不指定sourcepath ,linux环境下命令如下:
javac -d target $(find . -name "*.java")

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

csharp 复制代码
.
├── 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

shell 复制代码
java -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源文件,所有的包都在这个目录下创建。现在项目结构如下:

css 复制代码
.
├── 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目录,最终项目结构如下:

arduino 复制代码
.
├── 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

java 复制代码
public class User {

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

UserService.java

java 复制代码
public class UserService {
    public List<User> getUserList() {
        return User.createUserList();
    }
}

Server.java

java 复制代码
public class Server {

    public static void main(String[] args) throws JsonProcessingException {
        UserService service = new UserService();
        List<User> 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下使用;)。最终编译命令如下:

shell 复制代码
cd ~/workspace/mvn4java/project-3
javac -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下生成了包路径:

arduino 复制代码
└── target
    └── cn
        └── com
            └── notnull
                └── mvn4java
                    ├── model
                    │   └── User.class
                    ├── Server.class
                    └── service
                        └── UserService.class
运行

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

shell 复制代码
java -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,这里不能使用通配符,依赖之间使用空格分隔。

vbnet 复制代码
Manifest-Version: 1.0
Main-Class: cn.com.notnull.mvn4java.Server
Created-By: Manual-Packaging
Class-Path: lib/jackson-annotations-2.20.jar lib/jackson-core-2.20.0.jar lib/jackson-databind-2.20.0.jar
shell 复制代码
jar -cvfm Server.jar MANIFEST.MF -C target/ .

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

shell 复制代码
java -jar Server.jar
json = [{"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 /server
java -jar Server.jar
Error: A JNI error has occurred, please check your installation and try again
Exception 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就可以正常运行。

vbscript 复制代码
server
├── lib
│   ├── jackson-annotations-2.20.jar
│   ├── jackson-core-2.20.0.jar
│   ├── jackson-databind-2.20.0.jar
└── Server.jar
shell 复制代码
java -jar Server.jar
json = [{"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是项目根目录 -->
<directory>${project.basedir}/target</directory>
<outputDirectory>${project.build.directory}/classes</outputDirectory>
<finalName>${project.artifactId}-${project.version}</finalName>
<testOutputDirectory>${project.build.directory}/test-classes</testOutputDirectory>
<sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>
<scriptSourceDirectory>${project.basedir}/src/main/scripts</scriptSourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/java</testSourceDirectory>

阶段四:maven构建

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

shell 复制代码
mvn 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 复制代码
<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/maven-v4_0_0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>cn.com.notnull.mvn4java</groupId>
    <artifactId>project-4</artifactId>
    <packaging>jar</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>project-4</name>

    <dependencies>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.20.0</version>
        </dependency>
    </dependencies>
</project>

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

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

csharp 复制代码
[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目录:

arduino 复制代码
└── 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"运行,运行成功

json 复制代码
json = [{"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-pluginmaven-jar-plugin的职责很单一:将项目编译好的 class 文件(位于 target/classes)和资源文件(位于 src/main/resources)打包成一个标准的、不可直接执行的 JAR 文件,这个 JAR 文件只包含你自己的代码,不包含任何第三方依赖,跟之前的普通打包结果是一样。

maven-jar-plugin插件配置主类

plugin:

xml 复制代码
<plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <!-- 对要打的jar包进行配置 -->
        <configuration>
            <archive>
                <manifest>
                    <!-- !!!关键:指定 Class-Path,让JVM知道去哪里找依赖 !!! -->
                    <addClasspath>true</addClasspath>
                    <!-- 依赖JAR包所在的目录前缀 -->
                    <classpathPrefix>lib/</classpathPrefix>
                    <!-- 指定你的主类 -->
                    <mainClass>cn.com.notnull.mvn4java.Server</mainClass>
                </manifest>
            </archive>
        </configuration>
    </plugin>

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

makefile 复制代码
Manifest-Version: 1.0
Class-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.8
Created-By: Maven JAR Plugin 3.4.1
Main-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 复制代码
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>3.5.1</version> <!-- 请使用最新版本 -->
    <executions>
        <execution>
            <phase>package</phase> <!-- 绑定到 Maven 的 package 生命周期阶段 -->
            <goals>
                <goal>shade</goal>   <!-- 执行 shade 目标 -->
            </goals>
            <configuration>
                <!-- 这里是具体的配置 -->
                <transformers>
                    <!-- 指定可执行 JAR 的主类 -->
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <mainClass>cn.com.notnull.mvn4java.Server</mainClass>
                    </transformer>
                </transformers>
            </configuration>
        </execution>
    </executions>
</plugin>

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

json 复制代码
project-4-1.0-SNAPSHOT.jar
original-project-4-1.0-SNAPSHOT.jar

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

json 复制代码
json = [{"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项目的"全流程管理专家",从依赖到部署、从单人开发到团队协作,全方位提升开发效率与可靠性。

扩展

IDEA帮我们做了什么?
SpringBoot为什么可以直接运行?
其他构建工具
相关推荐
10km2 小时前
java:延迟加载实现方案对比:双重检查锁定 vs 原子化条件更新
java·延迟加载·双重检查锁定
独自归家的兔2 小时前
千问通义plus - 代码解释器的使用
java·人工智能
嘟嘟w2 小时前
什么是UUID,怎么组成的?
java
通往曙光的路上2 小时前
认证--JSON
java
期待のcode2 小时前
springboot热部署
java·spring boot·后端
222you3 小时前
Spring框架的介绍和IoC入门
java·后端·spring
毕设源码-朱学姐3 小时前
【开题答辩全过程】以 基于Java的人体骨骼健康知识普及系统为例,包含答辩的问题和答案
java·开发语言
喵手3 小时前
集合框架概述:让数据操作更高效、更灵活!
java·集合·集合框架
Java爱好狂.3 小时前
如何用JAVA技术设计一个高并发系统?
java·数据库·高并发·架构设计·java面试·java架构师·java八股文