首页
学习
活动
专区
圈层
工具
发布

Maven 项目打包:实现业务代码与第三方依赖分离

target 目录里那个 80MB 的 jar,我一般第一眼就不太舒服。

业务代码改了一行,发版却要重新上传一坨第三方依赖。Spring、MyBatis、Jackson、数据库驱动,全跟着业务 jar 一起滚。部署机网速稍微差点,传包比修 bug 还久。

这种 Maven 打包方式,开发环境没感觉,到了生产环境就开始烦人。

我更喜欢把包拆成这样:

app/

├── bin/

│   └── start.sh

├── lib/

│   ├── spring-core-xxx.jar

│   ├── mybatis-xxx.jar

│   └── mysql-connector-j-xxx.jar

├── conf/

│   └── application.yml

└── app-service.jar

app-service.jar只放业务代码。

第三方依赖全部丢到lib。

下次只改业务逻辑,直接替换一个几百 KB 或几 MB 的 jar 就行,不用把依赖重新打包一遍。

这事靠 Maven 做,不复杂,但有几个地方容易配歪。

先看pom.xml,我一般这么写:

  <finalName>app-service</finalName>

  <plugins>

      <!-- 只打业务代码 jar,不把依赖塞进去 -->

      <plugin>

          <groupId>org.apache.maven.plugins</groupId>

          <artifactId>maven-jar-plugin</artifactId>

          <version>3.3.0</version>

          <configuration>

              <archive>

                  <manifest>

                      <mainClass>com.dong.order.Bootstrap</mainClass>

                      <addClasspath>true</addClasspath>

                      <classpathPrefix>lib/</classpathPrefix>

                  </manifest>

                  <manifestEntries>

                      <Build-Time>${maven.build.timestamp}</Build-Time>

                  </manifestEntries>

              </archive>

          </configuration>

      </plugin>

      <!-- 把第三方依赖复制到 target/lib -->

      <plugin>

          <groupId>org.apache.maven.plugins</groupId>

          <artifactId>maven-dependency-plugin</artifactId>

          <version>3.6.1</version>

          <executions>

              <execution>

                  <id>copy-runtime-jars</id>

                  <phase>package</phase>

                  <goals>

                      <goal>copy-dependencies</goal>

                  </goals>

                  <configuration>

                      <outputDirectory>${project.build.directory}/lib</outputDirectory>

                      <includeScope>runtime</includeScope>

                      <excludeTransitive>false</excludeTransitive>

                  </configuration>

              </execution>

          </executions>

      </plugin>

  </plugins>

这里我比较在意两个点。

maven-jar-plugin负责打业务 jar,它会在MANIFEST.MF里写启动类和 classpath。

打完之后可以直接看:

jar xf target/app-service.jar META-INF/MANIFEST.MF

cat META-INF/MANIFEST.MF

正常应该能看到类似内容:

Main-Class: com.dong.order.Bootstrap

Class-Path: lib/jackson-databind-2.15.3.jar lib/mybatis-3.5.13.jar

如果这里没有Class-Path,启动时报ClassNotFoundException,别急着怀疑代码,先看这个文件。

maven-dependency-plugin负责把依赖复制出来。注意我这里用的是runtime,不是compile。

有些依赖编译时需要,运行时不一定要。反过来也有,比如数据库驱动,运行时才暴露问题。这个范围别乱写,不然后面启动时少 jar,日志会很直接:

Exception in thread "main" java.lang.NoClassDefFoundError:

com/mysql/cj/jdbc/Driver

业务启动类没必要写得很花。

package com.dong.order;

import java.nio.file.Files;

import java.nio.file.Path;

import java.time.LocalDateTime;

public class Bootstrap {

  public static void main(String[] args) throws Exception {

      Path config = Path.of("conf", "application.yml");

      if (!Files.exists(config)) {

          throw new IllegalStateException("配置文件不存在: " + config.toAbsolutePath());

      }

      System.out.println("order service starting, time=" + LocalDateTime.now());

      System.out.println("config loaded from " + config.toAbsolutePath());

      OrderServer server = new OrderServer(config);

      server.start();

  }

}

我会刻意让程序从外部conf目录读配置。

不要什么都塞进 jar。配置跟业务代码绑在一起,后面改个地址、改个开关,还要重新打包,这种设计迟早会把人恶心到。

打包命令很普通:

mvn clean package -DskipTests

执行完看target:

target/

├── app-service.jar

└── lib/

  ├── jackson-core-xxx.jar

  ├── jackson-databind-xxx.jar

  └── ...

如果要整理成部署目录,可以再加一个简单的脚本。这个不用搞太复杂,能看懂最重要。

#!/bin/bash

set -e

rm -rf target/release

mkdir -p target/release/bin

mkdir -p target/release/lib

mkdir -p target/release/conf

cp target/app-service.jar target/release/

cp target/lib/*.jar target/release/lib/

cp src/main/resources/application.yml target/release/conf/

cat > target/release/bin/start.sh <<'EOF'

#!/bin/bash

APP_HOME=$(cd "$(dirname "$0")/.." && pwd)

cd "$APP_HOME"

java \

-Xms512m \

-Xmx512m \

-Dfile.encoding=UTF-8 \

-jar app-service.jar

EOF

chmod +x target/release/bin/start.sh

这里有个坑。

很多人以为java -jar app-service.jar会自动加载旁边的lib目录。不会。

它认的是 jar 包里MANIFEST.MF的Class-Path。所以前面maven-jar-plugin那段配置不能漏。

还有一种启动方式是不用-jar,直接手写 classpath:

java -cp "app-service.jar:lib/*" com.dong.order.Bootstrap

Linux 下这么写没问题,Windows 要把冒号换成分号:

app-service.jar;lib/*

我平时更偏向第二种,直白。

尤其排查线上问题时,一眼就知道 JVM 到底加载了哪些目录。MANIFEST.MF那套也能用,但藏在 jar 里面,排查时要多拆一层。

最后再补一个我经常放进去的小检查,启动时把关键 class 从哪里加载的打印出来:

package com.dong.order.check;

public final class JarTrace {

  private JarTrace() {

  }

  public static void printLocation(Class<?> type) {

      String from = type.getProtectionDomain()

              .getCodeSource()

              .getLocation()

              .toExternalForm();

      System.out.println("[jar-trace] " + type.getName() + " -> " + from);

  }

}

启动时加两行:

JarTrace.printLocation(com.fasterxml.jackson.databind.ObjectMapper.class);

JarTrace.printLocation(com.dong.order.Bootstrap.class);

看到这种输出,心里就有底:

[jar-trace] com.fasterxml.jackson.databind.ObjectMapper -> file:/app/lib/jackson-databind-2.15.3.jar

[jar-trace] com.dong.order.Bootstrap -> file:/app/app-service.jar

这比猜半天强。

业务代码和第三方依赖分离,真正解决的不是“包大小”这一个问题。

它解决的是发版边界。

业务 jar 归业务,依赖 jar 归依赖,配置归配置。哪块变了动哪块。线上出了ClassNotFoundException、版本冲突、配置没生效,也知道先从哪里下手。

别把所有东西都揉成一个胖 jar,看着省事,排查的时候它会把账慢慢找回来。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/OHbIX0MCQR8oJtib5gDdAJ1g0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。
领券