从零散编译到一键打包:Maven如何重塑Java构建流程
灵魂拷问:不使用IDE工具,你能运行打包一个复杂的Java项目吗?
前置知识点
开始之前,先来了解两个JDK自带的工具,
javac和java,还有jar压缩包
javac
javac 是 Java 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之外,有Jython 、JRuby ,还有Kotlin 、Scala等语言,它们都实现了自己的"javac",可以编译成符合JVM规范的class文件,然后同样可以将这些语言编译出来的class文件运行于JVM上
jar
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包(类库包)。
我们导入的Junit、Lombok这些第三方jar包属于普通jar包,它们的MANIFEST.MF文件中不会描述Main-Class属性,因为它们不需要运行,只是作为一种类库导入给其他程序使用。而我们自己开发的应用程序,打包后通常属于可执行jar,需要通过Main-Class属性执行程序运行的入口类,然后可以使用java -jar xxx.jar执行该jar包。
为什么jar包存放class文件而不是java文件?
- JVM 只能执行字节码,无法直接运行源代码
- 字节码是跨平台的"中间语言" (实际上Java项目有时引入的第三方Jar包可能并不是Java编写的)
- 字节码是二进制文件,体积更小,适合分发和存储
- 简化部署,无需用户手动编译
注意事项
- 依赖冲突 :若多个 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 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下:
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包。具体步骤如下:
-
编译
shellcd ~/workspace/mvn4java/project-1 javac -d ./output Main.java -
在项目根目录创建清单文件 (MANIFEST.MF)
makefileManifest-Version: 1.0 Main-Class: cn.com.notnull.mvn4java.Main Created-By: Manual-Packaging -
使用JDK提供的jar工具打包
shelljar 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.mvn4java和cn.com.notnull.mvn4java.utils,并在主包下创建Main.java,在utils包下创建Sort.Java,Sort.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编译时怎么能够一次性将它们全部编译呢?又或者只编译某一个包的类?类之间有依赖关系怎么处理编译的先后顺序?
编译方式:
- 指定要编译的主类,
javac会自动找到主类所依赖的java源文件编译成class文件 - 遍历所有java源文件并传递给javac编译,javac会智能处理类之间的依赖关系
- 列表文件的方法,即将要编译的java文件列表写到一个文本文件里,让javac读取
- 一个一个源文件编译,不推荐,需要手动处理依赖关系,被依赖的类要先被编译
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-core和jackson-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-plugin,maven-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的世界里还有很多其他的打包插件可以使用。
- maven-shade-plugin: 创建通用的、可执行的 Fat JAR。
- maven-assembly-plugin: 高度可定制化地创建分发包,如 ZIP, TAR.GZ, Dir 等。适合复杂项目
- spring-boot-maven-plugin: 专为 Spring Boot 项目打造,创建可执行的 Fat JAR (或 Fat WAR)。
接下来选择maven-shade-plugin进行打包。
maven-shade-plugin核心用途
- 创建可执行 Fat JAR :这是最主要的功能。它将所有依赖的
.class文件打包进一个最终的 JAR 中,使得java -jar xxx.jar可以直接运行。 - 处理资源转换 (Transformations) :合并或转换在依赖中可能存在的同名文件,最经典的例子是合并
META-INF/services/目录下的文件,这对于 Java SPI (Service Provider Interface) 机制至关重要。 - 重命名类 (Relocating Classes):解决依赖冲突的神器。如果两个不同的依赖库包含了相同包名的类,可以通过重命名其中一个库的包路径来避免冲突。
- 创建无依赖的发行版:最终产物是一个独立的文件,便于分发和部署。
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的出现彻底改变了这一局面------它像一个"智能管家",帮开发者自动完成了:
- 依赖管理:不用手动下载jar包,通过坐标一键引入并解决依赖冲突;
- 标准化流程 :规范了项目结构(如代码放
src/main/java、资源放src/main/resources),统一了编译、测试、打包的步骤; - 一键构建 :只需敲
mvn compile(编译)、mvn exec:java(运行)、mvn package(打包),复杂操作(如生成包含所有依赖的可执行Fat JAR)全由插件自动处理; - 效率提升:告别手动管理jar包路径、依赖版本和构建顺序的繁琐,让开发者专注写业务代码。
简言之,Maven把原本需要"手敲几十行命令+反复调试"的Java构建流程,简化成了"配置一次+一键执行"的高效体验。除此之外,Maven还有更多强大功能:
-
多模块管理:支持将大型项目拆分为多个子模块(如核心模块、Web模块、工具模块),通过父子POM统一管理依赖和构建逻辑,避免重复配置;
-
生命周期与插件体系:内置清晰的构建生命周期(如clean、compile、test、package、install、deploy),通过丰富的插件(如生成项目文档的maven-javadoc-plugin、代码质量检查的maven-checkstyle-plugin)扩展功能;
-
仓库管理:自动从本地仓库或远程仓库(如Maven中央仓库)下载依赖,支持私有仓库部署,确保团队协作时依赖版本一致;
-
跨环境一致性:通过POM文件锁定依赖版本和构建配置,保证开发、测试、生产环境构建结果完全一致;
-
项目信息管理:在POM中定义项目描述、开发者信息、许可证等元数据,便于团队协作和项目维护。
Maven不仅是"构建工具",更是Java项目的"全流程管理专家",从依赖到部署、从单人开发到团队协作,全方位提升开发效率与可靠性。