可执行Jar案例

wen java案例 4

本文目录导读:

可执行Jar案例

  1. 📖 目录导读
  2. 什么是“可执行Jar”?—— 不只是“双击就能跑”那么简单
  3. 三大主流构建工具实操(Maven/Gradle/IDE)
  4. 真实案例:Spring Boot + 外部配置 + 多环境打包
  5. 可执行Jar的隐藏陷阱(类加载器、资源泄漏、瘦身方案)
  6. 生产环境部署:脚本化启动 + 优雅停机 + 日志切割
  7. 常见问题FAQ(面试高频 & 实战踩坑)
  8. 一劳永逸的发布流程设计

可执行Jar案例深度解析:从零构建到自动化部署的完整实战指南

📖 目录导读

  1. 什么是“可执行Jar”?—— 不只是“双击就能跑”那么简单
  2. 三大主流构建工具实操(Maven/Gradle/IDE)
  3. 真实案例:Spring Boot + 外部配置 + 多环境打包
  4. 可执行Jar的隐藏陷阱(类加载器、资源泄漏、瘦身方案)
  5. 生产环境部署:脚本化启动 + 优雅停机 + 日志切割
  6. 常见问题FAQ(面试高频 & 实战踩坑)
  7. 一劳永逸的发布流程设计

什么是“可执行Jar”?—— 不只是“双击就能跑”那么简单

很多初学者误以为“可执行Jar”仅仅是在打包时加了个Main-Class属性,真正的可执行Jar(Fat Jar / Uber Jar)需要解决依赖打包资源文件路径类加载顺序三大核心问题。

在经典的“瘦Jar”中,如果你的应用引用了10个第三方库,部署时就必须手动维护lib/目录并拼写冗长的classpath,而可执行Jar将所有.class.jar依赖重新压缩到一个单一文件中,真正实现了“单文件交付”。

关键原理:JVM规范中的JarFile支持嵌套Jar加载,Spring Boot Loader通过自定义JarURLConnection实现了jar-in-jar的透明读取,这意味你可以直接用:

java -jar my-app.jar

而无需管里面的依赖在哪里。


三大主流构建工具实操(Maven/Gradle/IDE)

案例A:Maven maven-shade-plugin(最通用)

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>3.4.1</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals><goal>shade</goal></goals>
            <configuration>
                <transformers>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <mainClass>com.example.Main</mainClass>
                    </transformer>
                </transformers>
                <!-- 排除签名文件,避免SecurityException -->
                <filters>
                    <filter><artifact>*:*</artifact>
                        <excludes>
                            <exclude>META-INF/*.SF</exclude>
                            <exclude>META-INF/*.DSA</exclude>
                        </excludes>
                    </filter>
                </filters>
            </configuration>
        </execution>
    </executions>
</plugin>

案例B:Gradle Shadow 插件(更简洁)

plugins { id 'com.github.johnrengelman.shadow' version '8.1.1' }
jar { manifest { attributes 'Main-Class': 'com.example.Main' } }

执行 ./gradlew shadowJar,输出-all.jar即可直接运行。

案例C:IDE内置(IDEA/Eclipse)

IDEA中:File → Project Structure → Artifacts → + → JAR → From modules with dependencies → Main Class → Build,此方法适合快速原型,但不适合CI/CD,因为IDE环境不可重复。


真实案例:Spring Boot + 外部配置 + 多环境打包

需求背景:某支付服务需要根据dev/prod环境切换数据库地址,且生产环境不能重新打包。

步骤1:配置文件分离

# application.yml
server:
  port: 8080
spring:
  profiles:
    active: @profile.active@  # 通过Maven profile动态填充

步骤2:Maven多Profile配置

<profiles>
    <profile>
        <id>dev</id>
        <properties><profile.active>dev</profile.active></properties>
    </profile>
    <profile>
        <id>prod</id>
        <properties><profile.active>prod</profile.active></properties>
    </profile>
</profiles>

打包命令:mvn clean package -Pprod -DskipTests

步骤3:生产运行(外部化配置)

java -jar app.jar --spring.config.location=/etc/myapp/application-prod.yml

关键点:可执行Jar内嵌的配置文件只是“默认值”,外部配置优先级永远高于内部配置,这一机制使得运维无需修改jar包,只需维护一个外部yaml即可。


可执行Jar的隐藏陷阱(类加载器、资源泄漏、瘦身方案)

陷阱1:NoClassDefFoundError 或 ClassNotFoundException

原因:多个依赖中存在相同类的不同版本(冲突),使用mvn dependency:tree排查,用exclusions排除旧版。

陷阱2:静态资源读取失败

错误写法:new File("src/main/resources/data.txt"),正确方法:

ClassPathResource res = new ClassPathResource("data.txt");
InputStream is = res.getInputStream();

因为打包后资源在Jar内部,不再是文件系统路径。

陷阱3:瘦身方案

如果无法接受50MB的Fat Jar,使用spring-boot-maven-pluginrequiresUnpack精确控制哪些依赖需要解压,或者使用layertools分离依赖层:

java -Djarmode=layertools -jar app.jar extract

然后通过docker build时逐层缓存,大幅减少镜像推送时间。


生产环境部署:脚本化启动 + 优雅停机 + 日志切割

启动脚本(配合systemd)

#!/bin/bash
JAR_PATH=/opt/myapp/app.jar
PID_FILE=/var/run/myapp.pid
case $1 in
  start)
    nohup java -Xms512m -Xmx1g -jar $JAR_PATH --spring.config.location=/etc/myapp/ > /var/log/myapp/stdout.log 2>&1 &
    echo $! > $PID_FILE ;;
  stop)
    # 优雅停机:Spring Boot 2.3+自带 /actuator/shutdown
    curl -X POST http://localhost:8080/actuator/shutdown -H 'X-Forwarded-For: internal' 
    ;;
  restart)
    $0 stop; sleep 2; $0 start;;
esac

日志切割(logrotate)

/var/log/myapp/*.log {
    daily
    rotate 7
    compress
    delaycompress
    copytruncate
}

常见问题FAQ(面试高频 & 实战踩坑)

Q1:java -jarjava -cp 启动有什么区别?
A:-jar 忽略-cp,仅依据Manifest中的Class-PathMain-Class-cp手动管理所有类,可执行Jar内嵌了所有依赖,所以无需外部-cp

Q2:为什么我打包后运行提示“没有主清单属性”?
A:maven-jar-plugin默认不包含Main-Class,必须配合maven-shade-plugin(第2节)或直接修改jar插件的manifest配置。

Q3:可执行Jar内的日志文件能写到当前目录吗?
A:写法相同,但注意磁盘权限,更推荐日志通过logback${user.home}/logs或外部挂载卷,避免容器文件系统碎片化。

Q4:如何降低可执行Jar的启动内存?
A:使用-XX:+TieredCompilation -XX:TieredStopAtLevel=1 -Xverify:none(JDK8),或者改用GraalVM原生镜像。

Q5:Spring Boot可执行Jar能否直接解压修改资源后重新打包?
A:不推荐,Jar是ZIP格式但包含“缺失的头部”定位信息,手工修改会破坏内部索引,正确做法是使用“提取→修改→重新打包”的layertools插件。


一劳永逸的发布流程设计

一个高质量的可执行Jar发布流程应具备:

  1. 可重复构建:基于Maven/Gradle,锁定依赖版本,拒绝使用SNAPSHOT
  2. 外部化配置:环境相关一切参数从环境变量或外部配置文件注入。
  3. 版本化与回滚:使用git tag记录Jar SHA256值,部署时通过软链切换版本。
  4. 安全加固:使用jarsigner对Jar签名,防止篡改。

最终验收标准:新同事拿到jar包,不阅读任何文档,仅凭--help和默认配置就能在5分钟内启动服务并看到健康检查通过。

你可以自信地告诉团队:“从今天起,我们的部署只有一条命令:scp app.jar target:/opt/myapp/ && ssh target ./restart.sh”。

抱歉,评论功能暂时关闭!