文章目录

  • 简介
  • 创建测试项目
  • maven-jar-plugin
  • 打可执行包
  • 依赖在哪里?
  • maven-assembly-plugin
  • maven-shade-plugin
  • spring-boot-maven-plugin
  • mvn打包一个比较坑的问题
  • 打包问题排查


简介

很多时候我们不太会关心maven是如何打包的,因为maven的确做得很棒,提供了很多默认的设置。

就算需要一些不同的定制,基本上也能找到插件拷贝一下就好了。

但是,当打包遇到一些比较特殊的情况,或者要分析一下jar包,我们就需要对Maven打包有多一点的了解。

本文将介绍Maven常见的打包插件,及其打包的情况,相信能帮你多了解一点Maven的打包操作。

先做一个简单的小结:

  1. maven-jar-plugin:默认使用,打依赖包啥都不用配置,打可执行包要配置main-class,它只能打依赖和可执行分离包
  2. maven-assembly-plugin、maven-shade-plugin:若果希望把依赖也打到可执行包中,可以使用这2个插件
  3. 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

maven项目外部包打包打不进去 maven无法打包_java


我们可以看到打出的包很小,看一下包内情况:

maven项目外部包打包打不进去 maven无法打包_maven_02


可以看到没有依赖包。

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

maven项目外部包打包打不进去 maven无法打包_maven_03

如果觉得默认的名字不是自己想要的,可以在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!");
    }
}

maven项目外部包打包打不进去 maven无法打包_maven项目外部包打包打不进去_04


这是因为默认不会打依赖包,所以运行时找不到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使用:分割

maven项目外部包打包打不进去 maven无法打包_SpringBoot打包_05

那如何能把依赖的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项目外部包打包打不进去 maven无法打包_maven_06

我们可以看到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打的包的结构还是比较清晰的:

maven项目外部包打包打不进去 maven无法打包_maven项目外部包打包打不进去_07

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项目外部包打包打不进去 maven无法打包_maven_08

如果,希望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了,很多组件基本都不需要这个了。

打包问题排查

为了快速方便解决问题,对于大多数人第一选择当然是搜索引擎,搜索关键字,找答案。

当我们搜索不到答案,或者搜索到的都是,大佬解决了吗?兄弟你最后是咋解决得?之类,就知道是时候为社区贡献自己力量的时候到了。

但是怎么解决问题呢?

  1. 看相关打包命令脚本,看一下实际的逻辑,检查是否是版本、或者环境原因
  2. 用where、which命令看一下脚本命令是否是自己配置了多个版本混用冲突了
  3. 看日志,耐心看一点不要放过,最好输出debug的日志、看一下到底出错在哪一步
  4. 如果上面信息都没有解决,那只能去看源码了,找到对应的错误位置,去检查对应的源码,自己动手调试