ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JAR包自动启动脚本完全指南:从systemd到NSSM的实战方案

JAR包自动启动脚本完全指南:从systemd到NSSM的实战方案 做服务部署这些年我越来越觉得“写启动脚本”这件事是区分“能用”和“好用”的分水岭。你手动敲一句java -jar app.jar服务能起来可一旦服务器重启、进程崩溃、SSH窗口一关剩下的全是麻烦。我印象最深的一次是凌晨两点被电话叫醒被告知线上服务挂了登上去一看就是当初“图省事”没做自动拉起惹的祸。从那以后我给所有JAR包服务都配了自动启动脚本无论是Linux还是Windows绝不手动裸跑。这篇就专门聊聊JAR包自动启动脚本怎么设计、怎么写、怎么避坑把我实操过的方案完整地摆出来。1. 整体设计与思路拆解1.1 先想清楚“自动启动”到底要解决什么问题很多人一听到“JAR包自动启动脚本”第一反应就是“写个脚本开机帮我跑一下java -jar”。如果只是这个目标那脚本十分钟就能写完。但实际运维里我们需要的远不止“开机启动”还有服务崩溃后的自动恢复、日志的归集与轮转、停止时的优雅退出、以及人工巡检时的状态检查入口。我统计了一下自己维护过的服务真正需要手动干预的情况里大概三成是机器重启四成是JVM内存溢出或进程被OOM Killer干掉剩下三成是部署更新时停错了进程。这三类问题其实都能通过一套设计好的启动脚本解决大半。换句话说自动启动脚本表面上是“启动命令的封装”本质上是一套“进程生命周期管理方案”。我自己在设计这类脚本时会把需求拆成五点开机自启机器重启后不用人工登录服务自动就位。崩溃自启进程异常退出后能在一段时间内自动拉起而不是等告警了再处理。日志可追溯标准输出、错误输出都落到固定文件并且按天或按大小轮转。状态可查随时能通过一条命令看到“服务到底活着没”。优雅停机执行停止操作时给JVM留出处理收尾工作的时间尽量避免直接kill -9。如果把最初那句“帮我跑一下”扩展成这五条需求你再看市面上的方案思路就清晰多了。1.2 方案选型systemd、Shell脚本、Windows服务各有各的适用场景不同操作系统、不同部署规模适合的做法完全不一样。我实际用下来大致可以分成三条路线方案适用平台核心思路优点缺点systemd服务LinuxCentOS 7、Ubuntu 16等把JAR包注册成系统服务开机自启、崩溃重启、日志管理全部内置配置简洁需要理解systemd的unit语法Shell脚本 nohup/cron通用Linux尤其是不方便改系统的环境自己写start/stop/status灵活可控依赖少纯bash也能跑崩溃自启需要额外机制比如while循环或cron轮询NSSM / 计划任务Windows Server将JAR包注册成Windows服务或通过计划任务触发Windows下最稳能跟随系统启动需要额外下载工具或配置计划任务我在Linux生产环境上首选systemd因为它把“守护进程”这件事做得非常彻底。Restartalways加上RestartSec在进程崩溃后会自动等待几秒再拉起比我们自己写循环判断要可靠得多。但systemd不是万能的比如在容器环境里、或者公司内部某些禁用了systemd权限的机器上我还是会用Shell脚本兜底。Windows下则优先用NSSM因为它把JAR包变成真正的Windows服务不依赖某个用户是否登录。选型上我的建议很直接能用systemd就用systemd没有systemd就用Shell脚本Windows用NSSM。别在方案上纠结太久后面我会把三条路线都给出可复用的配置。1.3 自动启动脚本的本质包装脚本与服务注册分离这里有个容易被忽视的设计思路我直到维护了多个项目后才想明白启动脚本应该拆成两层。第一层是“服务注册”负责告诉操作系统“这个服务该怎么管”比如systemd的service文件、Windows的服务注册表或者cron的定时任务。第二层是“启动包装脚本”负责真正的Java命令组装包括classpath、JVM参数、启动参数、日志重定向等。为什么一定要拆开因为系统级的服务管理工具对Java进程的了解是有限的它只负责拉起和监控进程状态至于“用哪个JDK、堆内存给多大、GC用什么策略”这些必须由你定义的启动命令来决定。如果直接把所有java参数堆在systemd文件里以后想调整JVM参数还得改系统配置然后daemon-reload维护成本高。我把JVM参数和启动命令都放在一个start.sh里systemd只负责调用这个脚本后面任何一个环境想复用比如从systemd改成手动执行完全不需要改Java部分的逻辑。这个“分离”的思路虽然简单但用过的人都知道它能帮你省下大量重复修改配置的时间。2. 核心细节解析与实操要点2.1 先搞懂java -jar是怎么把代码跑起来的写启动脚本之前得先弄明白java -jar背后发生了什么不然出了问题连排查方向都没有。当你执行java -jar app.jar时JVM会去读这个JAR包里的META-INF/MANIFEST.MF文件找到Main-Class字段指定的类然后用这个类作为入口开始执行。所以一个JAR包能不能直接跑起来关键看打包时有没有正确地写入Main-Class。如果你是用Spring Boot的Maven插件打包的它会自动生成一个可执行的fat JAR把项目的所有依赖都塞进同一个JAR包里所以java -jar能直接找到所有类。但如果是普通项目或者依赖没有完全打进去运行时就会报ClassNotFoundException。不少朋友问过“jar包怎么缝合”其实这就是打fat JAR的问题。在Spring Boot项目里Maven的spring-boot-maven-plugin的repackage目标会把原JAR和所有依赖合并成一个可分发的JAR这个过程本质上就是“缝合”。在非Spring Boot项目里你可以用maven-assembly-plugin或shade-plugin达到类似效果。如果缝合这一步没做好启动脚本写得再漂亮也没用因为JVM压根找不到依赖类。还有一类情况你的项目依赖了某些不在Maven中央仓库的本地JAR比如厂商提供的SDK。这时除了在IDE里通过“Project Structure - Libraries”引入更规范的做法是在Maven里使用system作用域或安装到本地仓库。这样打包出来的JAR才是完整的后面自动启动才能稳定复现。理解了这些你就明白为什么启动脚本里通常只要有java -jar app.jar这么一行但它能跑通的前提是JAR包本身是完整可执行的。脚本解决的是“怎么拉起来”的问题JAR包解决的是“拉起来后能不能活”的问题。2.2 JVM参数怎么给别拍脑袋先算内存账启动脚本里最重要的就是JVM参数尤其是堆内存配置。我看到很多新手喜欢写-Xmx1024m这种固定值上线后流量稍微一涨就是OutOfMemoryError或者反过来一台16G内存的机器只给JVM分了256M资源浪费严重。我的做法是先把机器的物理内存算清楚再结合服务的特点分配。比如一台8G内存的ECS操作系统本身和监控Agent大概占用1G到1.5G文件页缓存也需要一些空间那么JVM最多可以拿到6G左右。如果这是一个高并发但对象生命周期短的Web服务我通常把-Xmx和-Xms设成一样比如4G避免JVM运行时频繁扩容堆内存。这里有个经验值堆内存留出30%的余量给元空间、线程栈和直接内存。也就是-Xmx4g实际上JVM进程占用的RSS可能在5G到5.5G。所以机器8G内存-Xmx给4G比较稳给5G就容易在GC高峰期触发整机内存不足。常用的参数组合我贴一下可以直接抄JAVA_OPTS-server -Xms4g -Xmx4g -XX:MaxMetaspaceSize512m -XX:UseG1GC -XX:MaxGCPauseMillis200 -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/data/logs/app.hprof解释一下每个参数的用途-server使用服务器模式JIT编译更激进对长时间运行的服务器程序更友好。-Xms4g -Xmx4g初始堆与最大堆一致减少运行期扩容开销。-XX:MaxMetaspaceSize512m限制元空间防止类加载过多导致内存无限增长。-XX:UseG1GCJDK 8之后的通用垃圾收集器兼顾吞吐和停顿时间适合多核大内存机器。-XX:MaxGCPauseMillis200给G1一个停顿目标让它在吞吐和延迟之间做权衡。-XX:HeapDumpOnOutOfMemoryErrorOOM时自动导出堆快照排查内存问题必备。启动参数这块我多说一句千万不要直接复制网上随便找的“标准参数”尤其是那些把-Xmn、-XX:SurvivorRatio全部写死的配置没有分析业务就直接套用反而会把性能调差。先跑起来用jstat观察一段时间的GC频率再微调才是正路。2.3 启动包装脚本里必须处理的几件事JVM参数准备好后包装脚本里还需要处理几个细节这些细节决定了脚本好不好用。第一是PID文件。启动时把进程ID写到一个固定的app.pid文件中stop的时候从这个文件读PID然后kill。这个看似简单的设计能避免很多“停错进程”的事故。没有PID文件的话你就只能用jps或pgrep去匹配进程名万一机器上有多个同名JAR包很容易误杀。第二是日志重定向。直接执行java -jar日志输出到当前终端一旦终端断开会话进程可能会收到SIGHUP。所以脚本里至少要做nohup java -jar ... app.log 21 让进程脱离终端并把标准输出和标准错误都落到文件。生产环境我还会额外配logback或log4j2的滚动策略让应用日志按天滚动避免单个日志文件无限膨胀。第三是健康检查。进程在并不代表服务可用Spring Boot应用有可能端口起来了但仍在初始化。所以我习惯在启动脚本里追加一个check函数通过访问应用的/actuator/health接口或检查TCP端口来判断状态。这个函数在启动后自动调用一次也会被status命令复用。第四是优雅停机。JVM接收SIGTERM信号后Spring Boot会有默认的优雅停机逻辑但默认不是完全优雅的。建议在启动参数里加-Dspring.lifecycle.timeout-per-shutdown-phase30s同时脚本stop时先发kill即SIGTERM等待一段时间后再确认进程是否退出不要上来就kill -9。3. 实操过程与核心环节实现3.1 Linux下用systemd托管JAR包最省心的方案如果你的Linux发行版有systemd我的首选就是它。配置好后开机自启和崩溃重启都是系统级别保证的比任何自己写的循环都可靠。先建一个服务单元文件假设服务名叫myappcat /etc/systemd/system/myapp.service EOF [Unit] DescriptionMy Application Service Afternetwork.target mysqld.service [Service] Typesimple Userappuser WorkingDirectory/opt/myapp EnvironmentFile/etc/myapp.conf ExecStart/opt/myapp/bin/start.sh ExecStop/bin/kill -s TERM $MAINPID Restartalways RestartSec5 StartLimitInterval60 StartLimitBurst5 [Install] WantedBymulti-user.target EOF然后执行systemctl daemon-reload systemctl enable myapp systemctl start myapp systemctl status myapp这里有几个细节值得展开。Typesimple表示ExecStart启动的进程就是主进程。因为start.sh里最终会用exec java -jar ...来运行所以systemd能直接跟踪到Java进程本身。如果不用exec而是nohup java -jar ... systemd会认为主进程已经退出导致Restartalways反复重启这是一个很隐蔽的坑。EnvironmentFile/etc/myapp.conf可以放环境变量比如SPRING_PROFILES_ACTIVEprod、JAVA_HOME等。这样环境变量和启动脚本分离部署时只改配置文件不用动service文件。ExecStop/bin/kill -s TERM $MAINPID是发送SIGTERM信号给主进程让Spring Boot走优雅停机流程。Restartalways配合RestartSec5表示进程崩溃后5秒自动拉起重试。StartLimitBurst和StartLimitInterval限制短时间内反复崩溃时的拉起次数防止“启动失败-重启-再失败”死循环。对应的start.sh内容是这样#!/bin/bash cd /opt/myapp exec java $JAVA_OPTS -jar myapp.jar --spring.config.location/opt/myapp/config/务必要用exec把Java进程替换成当前Shell进程。这样PID就等于start.sh的PIDsystemd的$MAINPID才能准确指向Java进程。3.2 没有systemd时用Shell脚本写一套完整的启停控制有些环境比较特殊比如公司内部的老系统不给systemd权限或者你只是在一台跳板机上临时部署一个分析工具。这时候Shell脚本反而是最通用的方案。我在这种场景下长期使用过下面这套脚本功能覆盖启动、停止、重启、状态查看和崩溃自动拉起。#!/bin/bash APP_NAMEmyapp APP_HOME/opt/myapp JAR_FILE$APP_HOME/myapp.jar PID_FILE$APP_HOME/myapp.pid LOG_FILE$APP_HOME/logs/myapp.log JAVA_OPTS-server -Xms1g -Xmx1g -XX:UseG1GC -XX:HeapDumpOnOutOfMemoryError start() { if [ -f $PID_FILE ]; then pid$(cat $PID_FILE) if kill -0 $pid 2/dev/null; then echo $APP_NAME already running, pid$pid exit 1 fi fi cd $APP_HOME nohup java $JAVA_OPTS -jar $JAR_FILE $LOG_FILE 21 echo $! $PID_FILE echo $APP_NAME started, pid$(cat $PID_FILE) } stop() { if [ ! -f $PID_FILE ]; then echo pid file not found, maybe not running exit 1 fi pid$(cat $PID_FILE) kill $pid for i in $(seq 1 30); do if ! kill -0 $pid 2/dev/null; then rm -f $PID_FILE echo $APP_NAME stopped return 0 fi sleep 1 done kill -9 $pid rm -f $PID_FILE echo $APP_NAME force killed } status() { if [ ! -f $PID_FILE ]; then echo not running return 1 fi pid$(cat $PID_FILE) if kill -0 $pid 2/dev/null; then echo running, pid$pid return 0 else echo pid file exists but process not found, maybe crashed return 1 fi } restart() { stop sleep 2 start } case $1 in start) start ;; stop) stop ;; restart) restart ;; status) status ;; *) echo Usage: $0 {start|stop|restart|status} ; exit 1 ;; esac脚本的逻辑不复杂但有几个点我强调一下。nohup ... 之后立刻echo $! $PID_FILE这里的$!是刚启动的后台进程的PID也就是Java进程的PID。因为这里没有用exec所以脚本进程和Java进程是父子关系PID不同但子进程的PID就是$!可以放心写入。stop函数里我给了最多30秒的优雅退出时间。如果30秒内进程还没退再kill -9。这个设计比直接kill -9文明得多能给Spring Boot足够时间释放数据库连接池、完成正在处理的请求。至于崩溃自动拉起我实际使用中有两种方式。一种是在外面套一个while循环让这个脚本进入“守护模式”while true; do bash /opt/myapp/bin/myapp.sh status || bash /opt/myapp/bin/myapp.sh start sleep 10 done另一种是配置cron每分钟执行一次状态检查。两种方式我都在用cron方式更轻量但检查周期有延迟while循环方式响应快但会占用一个终端。3.3 Windows Server下运行JAR包别再只靠双击窗口热词里有“win server服务器运行jar包”这个场景我很熟悉。很多人在Windows上跑JAR包都是打开cmd窗口执行java -jar app.jar然后窗口不敢关就怕一关服务就没了。这种方式的毛病很明显服务器重启后没人登录服务起不来窗口被运维误关服务也直接消失。好一点的方案是写一个批处理配合Windows计划任务实现开机启动echo off cd /d D:\apps\myapp set JAVA_OPTS-server -Xms1g -Xmx1g -XX:UseG1GC start /B javaw %JAVA_OPTS% -jar myapp.jar D:\apps\myapp\logs\myapp.log 21注意这里用的是javaw而不是javajavaw启动时不会弹出黑色控制台窗口更适合后台服务。计划任务里配置“计算机启动时触发”这样重启后也能自动拉起。不过批处理加计划任务的方式对崩溃自启支持不好。如果进程异常退出计划任务不会自动重新拉起。所以我在Windows生产环境更推荐NSSM它能把任何可执行程序注册成真正的Windows服务。NSSM的使用非常简单nssm install MyApp C:\Program Files\Java\jdk-17\bin\java.exe -jar D:\apps\myapp\myapp.jar nssm set MyApp AppDirectory D:\apps\myapp nssm set MyApp AppStdout D:\apps\myapp\logs\myapp.log nssm set MyApp AppStderr D:\apps\myapp\logs\myapp-error.log nssm set MyApp AppRotateFiles 1 nssm set MyApp AppRotateBytes 10485760 nssm start MyApp注册成服务后服务的启动类型可以设置成“自动”开机自启由Windows服务管理器保证进程崩溃后可以在“服务属性 - 恢复”里配置“第一次失败重新启动服务”。比批处理方案强太多。NSSM还自带日志轮转AppRotateBytes设为10MB后日志超过大小自动归档省去了自己写轮转的麻烦。3.4 怎么验证自动启动脚本真的“自动”了脚本写完后别急着收工至少做三轮验证。第一轮是手动启动和停止确认进程状态、PID文件、日志输出都符合预期。第二轮是模拟崩溃用kill -9 pid强杀进程观察systemd或脚本能否自动拉起并检查拉起后的日志有没有报错。第三轮是模拟重启执行reboot等机器起来后登录直接systemctl status myapp或curl健康检查接口看服务是否自动恢复。我遇到过一种情况systemd配置了Restartalways但进程被强杀后并没有自动拉起。后来排查发现ExecStart启动的脚本里有nohup ... 导致systemd跟踪的主进程在启动成功后立刻退出Restartalways把“主进程退出”当成了“服务结束”所以反而不会重启。改成exec java ...后才恢复正常。这种细节不验证一次生产环境迟早踩雷。健康检查也值得一提如果应用有/actuator/health端点建议在systemd里配合ExecStartPost脚本做启动后的健康探测。没有的话就用ss -ltnp | grep 8080检查端口监听虽然浅层一些但至少能确认端口已经起来了。4. 常见问题与排查技巧实录4.1 “no main manifest attribute”JAR包本身就没打对这是最经典的一个报错执行java -jar app.jar直接提示no main manifest attribute, in app.jar很多人第一反应是“脚本写错了”其实和脚本一点关系都没有。问题出在JAR包的MANIFEST.MF里没有声明Main-Class。通常是普通Java项目用jar命令直接打的包或者Maven打包时没有配置mainClass。解决办法有两种第一种是在Maven的spring-boot-maven-plugin或maven-jar-plugin里显式声明主类第二种是不依赖JAR包里的Main-Class用java -cp app.jar com.example.MainClass直接指定入口函数。第二种方式我在排查问题时经常用因为它能绕开打包配置问题快速验证能跑的类。另外要注意如果项目使用了外部依赖但没有打成fat JARjava -jar启动后会在访问依赖类时抛ClassNotFoundException。这种“找不全类”的问题往往是打包时没有把依赖一起“缝合”进来需要检查是不是用了assembly或shade插件。4.2 “ClassNotFoundException: com.mysql.jdbc.Driver”与JDBC驱动问题热词里有“mysql数据库jar包下载”这个我深有体会。一个常见场景是本地开发用IDE跑得好好的部署到服务器上执行java -jar就报找不到MySQL驱动。原因基本可以锁定在依赖没有打入最终产物上。MySQL的JDBC驱动包在Maven中坐标是dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version /dependency如果你是手动管理JAR包的传统项目可以去Maven中央仓库下载对应的JAR然后放到lib目录启动时用java -cp app.jar:lib/* com.example.MainClass来运行而不是-jar。用-cp启动时JVM不会读MANIFEST.MF里的Main-Class所以主类必须显式指定。还有一类非常常见的坑是MySQL 8.0之后驱动类改名为com.mysql.cj.jdbc.Driver旧代码里写com.mysql.jdbc.Driver虽然还能兼容但会告警。另外连接串里最好加上serverTimezoneAsia/Shanghai否则Java 8以上版本连数据库会因为时区问题报错。这些虽然不完全是启动脚本的锅但排查“起不来”的时候十次里有两三次是栽在数据库驱动上。4.3 端口被占用进程却看不到启动脚本运行后提示Web server failed to start. Port 8080 was already in use.但用netstat -tlnp | grep 8080查不到进程或者看到的是另一个服务的监听。这时候别急着改端口先确认是不是有残留的Java进程占用了端口。排查命令我常用这三条ss -ltnp | grep 8080 lsof -i :8080 ps -ef | grep java如果是之前的服务没有完全退出直接用kill -9 PID清掉旧进程再启动。如果端口被其他服务占用那就需要协调或者确实得改端口。这里也提醒一下写启动脚本时最好把端口做成外部配置比如通过--server.port8081指定避免硬编码在代码里改动时不用重新打包。4.4 进程kill不掉或者杀了又自动起来有朋友遇到过“执行kill之后进程还在”的情况其实可能有两个原因。第一个原因是你杀错了进程服务器上可能有多个同名的Java进程用PID文件定位最稳妥。第二个原因是进程被杀后又被某个守护机制拉起来了比如systemd的Restartalways或者自己写的while循环守护脚本。如果是systemd在管想彻底停服务正确命令是systemctl stop myapp而不是直接kill PID。systemctl stop会执行ExecStop里的命令发送SIGTERM给主进程停止后systemd也不会再来拉。同理用NSSM管理的服务应该用nssm stop MyApp而不是任务管理器里杀进程。还有一个小技巧如果kill杀不掉进程先看看进程状态是不是D不可中断的睡眠多半是卡在IO上了。这种状态连kill -9都无效基本只能等IO恢复或者重启机器。排查时可以用cat /proc/pid/status里的State字段确认。4.5 启动卡住不动怎么定位卡在哪启动脚本执行后日志一直停在某个地方不往下走最让人头疼。我用过的最快定位方法是给Java进程发一个线程转储看看所有线程停在哪里。先找到进程PID然后执行jstack pid thread_dump.txt在thread_dump.txt里搜索main线程看看它卡在哪个类哪个方法。如果是Spring Boot应用通常卡在数据库连接、Redis连接或者某个远程接口调用上。还有一招是jcmd pid Thread.print效果类似。针对“jar包查找调用链”如果启动时依赖了某个JAR包里的类但初始化顺序不对或循环依赖jstack能清晰看到线程栈。我之前排查过一个启动卡死问题就是从线程转储里发现主线程阻塞在一个第三方SDK的静态初始化方法上而这个SDK在启动时去连了一个不存在的配置中心超时时间设成了5分钟所以看起来就像“永远卡住了”。4.6 Windows下批处理窗口一关服务就没了这个问题非常典型。很多人用java -jar app.jar运行窗口关闭后进程就没了。原因在于java启动的进程依附于当前控制台控制台关闭时系统向进程发送了关闭事件进程随之退出。两个解决方向一个是前面说的NSSM注册成服务另一个是批处理中使用javaw并配合start /B让进程脱离当前控制台。但脱离控制台不代表它变成了服务服务器重启后还是不会自动启动需要再配计划任务。所以我的建议是在Windows上就把JAR包当成Windows服务来管用NSSM一次性解决“开机自启”和“崩溃重启”别再自己写批处理硬扛。4.7 关于“需要引哪个JAR包”这类依赖问题的通用排查思路热词里还有“spring ai连接千问平台需要引哪个jar包”“ideal springbootmaven导入外部jar包”这类问题本质上都是“依赖管理没理清”。我的建议是不要用猜的直接去项目的依赖管理文件里查。如果项目是Maven用mvn dependency:tree可以列出全部依赖树。如果某个类找不到就搜索它应该属于哪个group和artifact然后看依赖树里有没有。如果依赖被排除了需要找到排除它的位置并调整。如果项目是手动管理JAR包那就要保证lib目录下所有JAR都在并且通过-cp方式启动时能正确加载到。说起来“jar包查找调用链”也是一种类似能力当你的服务运行时报某个类的方法不存在除了看日志栈还可以用javap -c反编译看字节码或者直接看依赖冲突。不过这些属于运行时排错的进阶内容这里先点到为止。5. 我的一些额外心得文章写到这里自动启动脚本的框架和细节都覆盖了我再补充几个踩过坑后留下来的个人习惯。第一所有脚本文件都统一放到服务的bin目录下命名统一用myapp.sh或myapp.bat不要一会儿叫start.sh一会儿叫run.sh。运维最怕的就是同一个项目的不同环境脚本命名还不一样接手的人完全不知道从哪里找入口。第二PID文件、日志目录、堆转储目录这三个路径一定在脚本开头定义成变量并保证目录存在比如mkdir -p。我见过太多版本换台机器部署就开始报“目录不存在”就是因为这些路径散落在脚本各处没有集中管理。第三不管用systemd还是Shell脚本JVM参数的修改都要通过脚本或EnvironmentFile完成不要临时在命令行手敲。这样部署流水线上才能实现“改参数-提交-部署-生效”的全自动化而不是每次都登录服务器手动调整。第四新人接手时最常问的一句话是“我这个服务怎么起来的、怎么看日志、怎么重启”。如果我把这些信息都写进了脚本的注释和README他们根本不用来问我。这一点看起来是文档工作但实际省下的是整个团队的时间。JAR包自动启动脚本这件事说难不难说简单却藏着这么多细节。真正常年跟服务打交道的人都懂“启动”只是一瞬间而让它稳定、可控、可观测地“一直在那里”才是一套脚本真正的价值。希望这篇内容能帮你少踩几个坑把JAR包服务安安稳稳地跑起来。
返回列表