通常情况下,标准化的打包流程往往难以满足特定场景的需求。
本文以一个基于 Spring Boot 2.7.18 的工具工程的 pom 为例,介绍如何通过 Maven 多插件协同配置实现定制化打包。
需求分析
该工程对打包有以下特殊要求:
- 版本信息内聚:程序 jar 包不携带版本号等冗余信息,而是通过构建配置在程序内部动态输出版本号及构建时间
- 依赖外置管理:将程序依赖统一保存在独立的 lib目录下,便于依赖共享和更新
- 自定义输出结构:打包成果统一存放在 target/bin目录下,包含配置文件、精简后的程序 jar 包以及完整的 lib 依赖文件夹
pom 文件参考
|   1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
 | <?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>cn.tofuwine.kits</groupId>
    <artifactId>DatabaseExporter</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <maven.build.timestamp.format>yyyy-MM-dd HH:mm:ss</maven.build.timestamp.format>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.build.time>${maven.build.timestamp}</project.build.time>
        <project.build.resources.directory>${project.build.directory}/bin</project.build.resources.directory>
        <!-- 依赖项版本定义 -->
        <spring-boot.version>2.7.18</spring-boot.version>
    </properties>
    <dependencies>
        <!-- 程序依赖项 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <build>
        <finalName>${project.artifactId}</finalName>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
                <includes>
                    <include>**/*.properties</include>
                </includes>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.10.1</version>
                <configuration>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.2.2</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                            <classpathPrefix>lib/</classpathPrefix>
                            <mainClass>cn.tofuwine.kits.DatabaseExporterApplication</mainClass>
                        </manifest>
                    </archive>
                    <outputDirectory>${project.build.resources.directory}</outputDirectory>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <version>3.3.0</version>
                <configuration>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
                <executions>
                    <execution>
                        <id>copy-resources-for-package</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-resources</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${project.build.resources.directory}/config</outputDirectory>
                            <resources>
                                <resource>
                                    <directory>src/main/resources</directory>
                                    <filtering>true</filtering>
                                    <includes>
                                        <include>**/*.properties</include>
                                    </includes>
                                </resource>
                            </resources>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>3.3.0</version>
                <executions>
                    <execution>
                        <id>copy-dependencies</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${project.build.resources.directory}/lib</outputDirectory>
                            <overWriteReleases>false</overWriteReleases>
                            <overWriteSnapshots>false</overWriteSnapshots>
                            <overWriteIfNewer>true</overWriteIfNewer>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
 | 
 
核心配置解析
基础属性配置
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
 | <properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <maven.build.timestamp.format>yyyy-MM-dd HH:mm:ss</maven.build.timestamp.format>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.build.time>${maven.build.timestamp}</project.build.time>
    <project.build.resources.directory>${project.build.directory}/bin</project.build.resources.directory>
    <!-- 版本属性定义 -->
    <spring-boot.version>2.7.18</spring-boot.version>
</properties>
 | 
 
这段配置定义了几个关键属性:
- 编译版本控制:maven.compiler.source和maven.compiler.target指定 Java 编译版本为 JDK 8
- 构建时间:project.build.time定义构建时间,并使用maven.build.timestamp.format自定义构建时间的显示格式。需要注意构建时间默认为 UTC 时间,若希望显示北京时间可在程序输出时进行时区转换。
- 自定义输出目录:project.build.resources.directory将输出目录设置为target/bin,这是实现自定义输出结构的基础。
- 版本管理:统一管理依赖项的版本,便于集中更新。
定义 JAR 名
| 1
2
3
4
 | <build>
    <finalName>${project.artifactId}</finalName>
    <!-- ...其他配置... -->
</build>
 | 
 
使用 artifactId 作为最终的 JAR 名,生成的 JAR 不携带版本号信息,例如 DatabaseExporter.jar。
资源过滤
开启资源过滤后,程序在启动时会自动替换参数。(经测试该部分仅影响 IDE 启动,构建配置请参考资源插件配置
    
)
| 1
2
3
4
5
6
7
8
9
 | <resources>
    <resource>
        <directory>src/main/resources</directory>
        <filtering>true</filtering>
        <includes>
            <include>**/*.properties</include>
        </includes>
    </resource>
</resources>
 | 
 
Maven JAR 插件配置
官方文档:maven-jar-plugin
    
    
        
    
    
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
 | <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>3.2.2</version>
    <configuration>
        <archive>
            <manifest>
                <addClasspath>true</addClasspath>
                <classpathPrefix>lib/</classpathPrefix>
                <mainClass>cn.tofuwine.kits.DatabaseExporterApplication</mainClass>
            </manifest>
        </archive>
        <outputDirectory>${project.build.resources.directory}</outputDirectory>
    </configuration>
</plugin>
 | 
 
该插件是 Maven 打包的核心,其配置非常关键:
- addClasspath:自动将所有依赖添加到- MANIFEST.MF的 Class-Path 中。
- classpathPrefix:指定依赖的相对路径为- lib/,这是实现依赖外置的核心配置。
- mainClass:指定程序的入口类,使 JAR 包可以直接通过- java -jar命令运行。
- outputDirectory:指定输出目录为之前定义的- target/bin。
资源插件配置
官方文档:maven-resources-plugin
    
    
        
    
    
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 | <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-resources-plugin</artifactId>
    <version>3.3.0</version>
    <configuration>
        <encoding>${project.build.sourceEncoding}</encoding>
    </configuration>
    <executions>
        <execution>
            <id>copy-resources-for-package</id>
            <phase>package</phase>
            <goals>
                <goal>copy-resources</goal>
            </goals>
            <configuration>
                <outputDirectory>${project.build.resources.directory}/config</outputDirectory>
                <resources>
                    <resource>
                        <directory>src/main/resources</directory>
                        <filtering>true</filtering>
                        <includes>
                            <include>**/*.properties</include>
                        </includes>
                    </resource>
                </resources>
            </configuration>
        </execution>
    </executions>
</plugin>
 | 
 
资源插件的作用是将配置文件复制到指定位置:
- execution:定义了一个在- package阶段执行的任务。
- outputDirectory:指定配置文件输出目录为- target/bin/config。
- filtering:【重要】开启资源过滤,可以在配置文件中使用 Maven 属性。
- includes:仅包含- properties文件,可以根据实际需求调整。
输出到同一目录下的不同目录文件可以通过配置多个 resource 节点实现。 输出到不同目录时,应配置多个 execution 节点,每个节点对应一个输出目录。
依赖插件配置
官方文档:maven-dependency-plugin
    
    
        
    
    
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
 | <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-dependency-plugin</artifactId>
    <version>3.3.0</version>
    <executions>
        <execution>
            <id>copy-dependencies</id>
            <phase>package</phase>
            <goals>
                <goal>copy-dependencies</goal>
            </goals>
            <configuration>
                <outputDirectory>${project.build.resources.directory}/lib</outputDirectory>
                <overWriteReleases>false</overWriteReleases>
                <overWriteSnapshots>false</overWriteSnapshots>
                <overWriteIfNewer>true</overWriteIfNewer>
            </configuration>
        </execution>
    </executions>
</plugin>
 | 
 
依赖插件负责实现依赖外置管理:
- outputDirectory:指定依赖的输出目录为- target/bin/lib
- overWriteReleases/Snapshots设置是否覆盖已存在的发布版/快照版依赖
- overWriteIfNewer:如果有更新的版本则覆盖
动态输出版本及构建时间
在程序中可使用如下配置动态输出当前版本及构建时间:
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
 | import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
@Component
@Slf4j
public class StartupLogListener implements ApplicationListener<ApplicationStartedEvent> {
    @Value("${application.version:}")
    private String version;
    @Value("${application.build.time:}")
    private String buildTime;
    private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    private final ZoneId defaultZoneId = ZoneId.of("Asia/Shanghai");
    private final ZoneId utcZoneId = ZoneId.of("UTC");
    @Override
    public void onApplicationEvent(ApplicationStartedEvent event) {
        if (StringUtils.isNotBlank(buildTime)) {
            buildTime = LocalDateTime.parse(buildTime, formatter).atZone(utcZoneId)
                    .withZoneSameInstant(defaultZoneId)
                    .format(formatter);
        }
        log.info("Application Started: {} (Build Time: {})", version, buildTime);
    }
}
 | 
 
配置文件中如下定义:
| 1
2
 | application.version=${project.version}
application.build.time=${project.build.time}
 | 
 
若想实现配置文件的自动替换,需要开启资源过滤机制。参考 资源插件配置
    
。
打包后的目录
执行 mvn clean package 命令后,在 target/bin 目录下会生成以下结构:
| 1
2
3
4
5
6
7
 | target/bin/
├── DatabaseExporter.jar      # 精简后的程序jar包
├── config/
│   └── application.properties # 配置文件
└── lib/
    ├── spring-boot-starter-2.7.18.jar
    └── ...其他依赖jar包
 | 
 
使用方法
打包完成后,可以通过以下命令运行程序:
| 1
 | java -jar DatabaseExporter.jar
 | 
 
由于我们已经正确配置了 Class-Path,JVM 会自动从 lib 目录加载依赖,无需额外设置 classpath 参数。