文章目录
- 简介
- 创建测试项目
- maven-jar-plugin
- 打可执行包
- 依赖在哪里?
- maven-assembly-plugin
- maven-shade-plugin
- spring-boot-maven-plugin
- mvn打包一个比较坑的问题
- 打包问题排查
简介
很多时候我们不太会关心maven是如何打包的,因为maven的确做得很棒,提供了很多默认的设置。
就算需要一些不同的定制,基本上也能找到插件拷贝一下就好了。
但是,当打包遇到一些比较特殊的情况,或者要分析一下jar包,我们就需要对Maven打包有多一点的了解。
本文将介绍Maven常见的打包插件,及其打包的情况,相信能帮你多了解一点Maven的打包操作。
先做一个简单的小结:
- maven-jar-plugin:默认使用,打依赖包啥都不用配置,打可执行包要配置main-class,它只能打依赖和可执行分离包
- maven-assembly-plugin、maven-shade-plugin:若果希望把依赖也打到可执行包中,可以使用这2个插件
- spring-boot-maven-plugin:SpringBoot打可执行jar、war包
创建测试项目
创建一个maven项目,下面是pom文件
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>vip.meet</groupId>
<artifactId>maven-package-learn</artifactId>
<version>1.0.0</version>
<name>maven-package-learn</name>
<description>maven打包</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.1.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
创建一个可执行的main类:
package vip.meet.start;
public class Main {
public static void main(String[] args) throws Exception {
System.out.println("Hello World!");
}
}
maven-jar-plugin
maven-jar-plugin是Maven打包默认使用的插件,如果pom中啥都不配置,默认使用的就是该插件。
执行下面命令打包:
# clean表示先清除,package阶段会自动包含执行clean这个phase,所以不加也没有问题
mvn clean package
我们可以看到打出的包很小,看一下包内情况:
可以看到没有依赖包。
META-INF\MANIFEST.MF文件中也没有主类Main-Class属性:
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven 3.8.2
Built-By: tim
Build-Jdk: 21
java -jar命令执行jar文件时,Java运行时会读取MANIFEST.MF文件,找到Main-Class属性指定的类,并执行该类的 main() 方法
没有Main-Class属性,说明默认打出来的包,不是可运行的jar包,而是依赖包。
打可执行包
那如何打出可执行包呢?
配置一下manifest的mainClass属性即可。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
<mainClass>vip.meet.start.Main</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
这样META-INF\MANIFEST.MF文件中就有Main-Class了:
Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.3.0
Build-Jdk-Spec: 21
Main-Class: vip.meet.start.Main
这样我们执行下面的命令,就可以打印出我们的Hello World!了
java -jar maven-package-learn-1.0.0.jar
如果觉得默认的名字不是自己想要的,可以在build下的finalName指定名字,注意不是在插件中,也不需要加.jar后缀。
<build>
<finalName>custom-name</finalName>
</build>
这样就可以打出custom-name.jar包了。
依赖在哪里?
现在就完事大吉了吗?
当然不是!
现在还不能打依赖包。不信?
加入一个依赖:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
改一下Main类:
package vip.meet.start;
import org.apache.commons.lang3.StringUtils;
public class Main {
public static void main(String[] args) {
System.out.println(StringUtils.isNotEmpty("Hello"));
System.out.println("Hello World!");
}
}
这是因为默认不会打依赖包,所以运行时找不到commons-lang3这个包,自然找不到StringUtils类。
怎么办?改配置:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<!-- 也可以在这里指定打出包的名称,会覆盖build中的设置 -->
<!-- <finalName>${project.artifactId}-${project.version}-mjp</finalName> -->
<manifest>
<mainClass>vip.meet.start.Main</mainClass>
<!-- MANIFEST.MF添加依赖-->
<addClasspath>true</addClasspath>
<!-- MANIFEST.MF指定依赖路径-->
<classpathPrefix>../lib/</classpathPrefix>
</manifest>
</archive>
</configuration>
</plugin>
上面的配置是把依赖信息写到META-INF\MANIFEST.MF文件中。
够了吗?
不够,因为jar包中还是没有依赖jar包,所以还得配置一下拷贝资源插件。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
问题解决了吗?
解决了一半!因为依赖包并没有打包到jar包中,打分离包模式还可以,运行也需要-cp模式
# 不能用-jar,因为-jar会导致-cp失效,而我们需要-cp指定classpath
# 所以需要手动指定要运行的主类
java -cp maven-package-learn-1.0.0.jar;./lib/* vip.meet.start.Main
# java -jar maven-package-learn-1.0.0.jar
注意:-cp依赖,Windows 使用;分割,Linux使用:分割
那如何能把依赖的jar包也打包到jar包中呢?
可以使用maven-assembly-plugin插件
maven-assembly-plugin
pom中添加plugin插件:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<archive>
<manifest>
<mainClass>
vip.meet.start.Main
</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</execution>
</executions>
</plugin>
# 打包
mvn clean package
# 执行有依赖的包
java -jar maven-package-learn-1.0.0-jar-with-dependencies.jar
我们可以看到maven-assembly-plugin打出的包比较乱,是因为它把依赖的jar包解压出来添加到最终的jar包中了。
maven-shade-plugin
maven-shade-plugin和maven-assembly-plugin很像,会在maven-jar-plugin打的包上做二次打包。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>vip.meet.start.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
# 打包
mvn clean package
# 执行有依赖的包
java -jar maven-package-learn-1.0.0.jar
spring-boot-maven-plugin
spring-boot-maven-plugin主要是最对SpringBoot项目的打包插件。
添加一个SpringBoot启动类:
package vip.meet;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
pom中配置插件:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.1.1</version>
<configuration>
<mainClass>vip.meet.Application</mainClass>
<layout>JAR</layout>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
JAR表示打成jar包,必须大写,还可以设置为WAR、ZIP
# 打包
mvn clean package
# 执行有依赖的包
java -jar maven-package-learn-1.0.0.jar
MANIFEST.MF文件内容:
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven 3.8.2
Built-By: tim
Build-Jdk: 21
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: vip.meet.Application
Spring-Boot-Version: 3.1.1
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
spring-boot-maven-plugin打的包的结构还是比较清晰的:
BOOT-INF下的classes中是我们自己的类
BOOT-INF下的lib中是依赖的jar包
有朋友可能就会问了,既然SpringBoot可以把依赖包直接放在lib目录下,那我们是不是也可以直接把依赖包拷贝到maven-jar-plugin打出的包中的lib目录下呢?
答案是不能,spring-boot-maven-plugin能是因为它重新写了类加载器,不信可以看的MANIFEST.MF中的main-class是Main-Class: org.springframework.boot.loader.JarLauncher。
如果,希望maven-jar-plugin能,也需要重写类加载器。
如果能简单做到,maven-assembly-plugin和maven-shade-plugin肯定不会选择直接解压jar包的方式。
当然如果是war包可以让在WEB-INF目录下,有兴趣的朋友可以自己看一下打出的war包结构和MANIFEST.MF文件。
mvn打包一个比较坑的问题
执行下面mvn打包命令
mvn clean package
遇到一个:
Fatal error compiling: 无效的目标发行版: 21
一直以为是maven-compiler-plugin插件配置有问题,但是不管怎么试都没用。
用IDEA的maven插件compile、jar都没用问题。
使用-X参数,打印所有debug日志
mvn -X clean package
然后发现,使用的竟然是Java8,但是我java -version看明明是21啊。
然后去看了mvn脚本:
if not "%JAVA_HOME%"=="" goto OkJHome
for %%i in (java.exe) do set "JAVACMD=%%~$PATH:i"
goto checkJCmd
:OkJHome
set "JAVACMD=%JAVA_HOME%\bin\java.exe"
使用mvn脚本,它会先去检查JAVA_HOME,如果配置了JAVA_HOME,就会找%JAVA_HOME%\bin\java.exe
如果没有配置JAVA_HOME,就会直接在可执行PATH中找java.exe,如果找到,就用这个。
很不幸,虽然我配置了21的PATH,但是JAVA_HOME还是Java8的,所以mvn脚本使用了Java8当然编译不了21版本。
所以,现在没事真不用配置JAVA_HOME了,很多组件基本都不需要这个了。
打包问题排查
为了快速方便解决问题,对于大多数人第一选择当然是搜索引擎,搜索关键字,找答案。
当我们搜索不到答案,或者搜索到的都是,大佬解决了吗?兄弟你最后是咋解决得?之类,就知道是时候为社区贡献自己力量的时候到了。
但是怎么解决问题呢?
- 看相关打包命令脚本,看一下实际的逻辑,检查是否是版本、或者环境原因
- 用where、which命令看一下脚本命令是否是自己配置了多个版本混用冲突了
- 看日志,耐心看一点不要放过,最好输出debug的日志、看一下到底出错在哪一步
- 如果上面信息都没有解决,那只能去看源码了,找到对应的错误位置,去检查对应的源码,自己动手调试