如何在Spring Boot中异步执行外部进程,并确保后续任务按顺序执行?
- 内容介绍
- 文章标签
- 相关推荐
先说点废话:为什么我们要在 Spring Boot 里玩儿异步外部进程这个?
说白了就是想让那帮 .exe 小子别把我们的 Tomcat 给卡住。可谁知道, 代码一写出来就像打开了 栓Q了... 潘多拉的盒子——一堆FutureCountDownLatch@Async 像天上的星星一样乱七八糟地闪。
你猜怎么着? 我甚至在凌晨三点的咖啡馆里 听着咖啡机“嗞嗞”声,写下这段代码:

ExecutorService executorService = Executors.newSingleThreadExecutor;
Future> future = executorService.submit;
future.get; // 阻塞,等外部进程玩完
getMaps21; // 再干正事
executorService.shutdown;
方案一:@Async + ProcessBuilder
先给 SpringBoot 打上 @EnableAsync 的标记,染后在方法上贴个 @Async。堪起来高大上,却常常被人误以为“一键解决”。其实它只是把任务扔进线程池,外部进程还是会阻塞那个线程。
@Async
public void invokeExeFile {
try {
ProcessBuilder pb = new ProcessBuilder;
Process p = pb.start;
p.waitFor; // 阻塞当前 async 线程
log.info;
} catch {
log.error;
}
}
摆烂。 如guo你真的想要“顺序”,别忘了在 @Async 方法外面用 Future.get 或着 CountDownLatch.await 把它拉回来。
方案二:CommandLineRunner / ApplicationRunner 搞定启动后的“一锤子”任务
好吧... 注意⚠️:这两个接口的 run 方法本身就在主线程里施行, 如guo你不小心直接调用阻塞代码,那就相当于给 Tomcat 喂了一颗炸弹。
@Component
public class ExeRunner implements CommandLineRunner {
private final ExecutorService exec = Executors.newFixedThreadPool;
@Override
public void run throws Exception {
Future> f = exec.submit;
f.get; // 等待外部进程结束
getMaps21; // 后续业务
}
private void invokeExeFile { /* 同上 */ }
}
到底怎么保证顺序?两把锁来帮忙!
LATCH 法:
private CountDownLatch latch = new CountDownLatch;
@Async
public void invokeExeFile {
try {
Process p = new ProcessBuilder.start;
p.waitFor;
} finally {
latch.countDown; // 放行
}
}
public void afterExe throws InterruptedException {
latch.await; // 等待 latch 被释放
getMaps21;
}
Tips:
- 不要在 @Async 方法里直接抛异常,否则 latch 永远不倒数。
- If you love chaos, mix both Future & Latch – you'll get a beautiful mess.
- 记得在
@PreDestroy里关掉线程池,不然容器停不下来。
随手写点噪音:日志、 情绪、随机表情 😱🤖🙈
哎呀,我的咖啡者阝凉了还要调这个线程池大小!
⚡️紧急提醒⚡️: 别把 .exe 的路径写死,否则搬家时哭到天亮。
| # | 产品名称 | 支持异步方式 | 是否自带 Latch 支持? | 用户满意度 |
|---|---|---|---|---|
| 1 | AIO ExecMaster Pro | @Async + ExecutorService ProcessBuilder 原生支持 | No | 4.6/5 ★★★★☆ |
| 2 | Boom! Async Runner Lite | Simplified @Async 注解 | Yes | 4.2/5 ★★★☆☆ |
| 3 | CleverShell Integrator | Scripting + ThreadPoolExecutor | No | 4.8/5 ★★★★★ |
| 4 | DynaTask Scheduler X | ScheduledExecutor + CompletableFuture | Yes 4.4/5 ★★★★✩ | |
| ※以上排名纯属个人感受, 实际请自行测试 🚀🚀🚀. | ||||
实战演练:从零到有,一个完整示例
@SpringBootApplication
@EnableAsync
public class DemoApplication {
public static void main {
SpringApplication.run;
}
}
----------------------------------------------------
@Component
class StartupTask implements ApplicationRunner {
private final ExecutorService pool = Executors.newCachedThreadPool;
private final CountDownLatch latch = new CountDownLatch;
@Override
public void run throws Exception {
pool.submit;
latch.await; // 等待外部程序跑完再继续...
System.out.println;
doBusiness;
shutdown;
}
@Async
public void runExternal {
try {
Process p = new ProcessBuilder.inheritIO.start;
p.waitFor;
System.out.println;
} catch {
e.printStackTrace;
} finally {
latch.countDown; // 必须放这里否则可嫩卡死!
}
}
private void doBusiness { System.out.println; }
private void shutdown { pool.shutdownNow; }
}
坑点大集合🕳️🕳️🕳️:
- Pitfall 1:忘记在
@Async方法里捕获异常,导致整个线程池崩溃。 - Pitfall 2:使用单例 Service 时 把共享的 CountDownLatch 当成全局变量,一次调用成功后后面的调用永远卡住。
- Pitfall 3:ProcessBuilder 默认不继承父进程的 I/O, 你会堪不到仁和日志,只嫩自己读
.getInputStream. - Pitfall 4:SpringBoot 默认关闭钩子会等所you非守护线程结束,如guo你的线程池用了默认非守护线程,会导致容器停不下来。
- Pitfall 5:如guo你的 .exe 是 Windows GUI 程序, 它会弹出窗口阻塞控制台输出,让人抓狂。
- Pitfall 6:别忘了给外部娱乐加上施行权限,否则 Linux 环境下报 Permission denied。
- Pitfall 7:大量并发调用外部进程时 会出现 “Too many open files” 错误,需要调高系统文件句柄限制。
- Pitfall 8:记得给 Process 设置超时否则某些卡死的 exe 会让你的服务一直挂起。
- Pitfall 9:不要把业务代码直接写在 finally 块里那样即使外部进程失败也会继续施行后面的业务——除非你真的想这么干。
- Pitfall 10:如guo你用了 Docker 容器, 要确保容器内有对应的 exe 或娱乐,否则启动瞬间报错。
收尾感言——写代码是一场情绪过山车 🎢🎢🎢♀️♂️♀️♂️♀️♂️♀️♂️♀️💔💖💥🚀🚀🚀
先说点废话:为什么我们要在 Spring Boot 里玩儿异步外部进程这个?
说白了就是想让那帮 .exe 小子别把我们的 Tomcat 给卡住。可谁知道, 代码一写出来就像打开了 栓Q了... 潘多拉的盒子——一堆FutureCountDownLatch@Async 像天上的星星一样乱七八糟地闪。
你猜怎么着? 我甚至在凌晨三点的咖啡馆里 听着咖啡机“嗞嗞”声,写下这段代码:

ExecutorService executorService = Executors.newSingleThreadExecutor;
Future> future = executorService.submit;
future.get; // 阻塞,等外部进程玩完
getMaps21; // 再干正事
executorService.shutdown;
方案一:@Async + ProcessBuilder
先给 SpringBoot 打上 @EnableAsync 的标记,染后在方法上贴个 @Async。堪起来高大上,却常常被人误以为“一键解决”。其实它只是把任务扔进线程池,外部进程还是会阻塞那个线程。
@Async
public void invokeExeFile {
try {
ProcessBuilder pb = new ProcessBuilder;
Process p = pb.start;
p.waitFor; // 阻塞当前 async 线程
log.info;
} catch {
log.error;
}
}
摆烂。 如guo你真的想要“顺序”,别忘了在 @Async 方法外面用 Future.get 或着 CountDownLatch.await 把它拉回来。
方案二:CommandLineRunner / ApplicationRunner 搞定启动后的“一锤子”任务
好吧... 注意⚠️:这两个接口的 run 方法本身就在主线程里施行, 如guo你不小心直接调用阻塞代码,那就相当于给 Tomcat 喂了一颗炸弹。
@Component
public class ExeRunner implements CommandLineRunner {
private final ExecutorService exec = Executors.newFixedThreadPool;
@Override
public void run throws Exception {
Future> f = exec.submit;
f.get; // 等待外部进程结束
getMaps21; // 后续业务
}
private void invokeExeFile { /* 同上 */ }
}
到底怎么保证顺序?两把锁来帮忙!
LATCH 法:
private CountDownLatch latch = new CountDownLatch;
@Async
public void invokeExeFile {
try {
Process p = new ProcessBuilder.start;
p.waitFor;
} finally {
latch.countDown; // 放行
}
}
public void afterExe throws InterruptedException {
latch.await; // 等待 latch 被释放
getMaps21;
}
Tips:
- 不要在 @Async 方法里直接抛异常,否则 latch 永远不倒数。
- If you love chaos, mix both Future & Latch – you'll get a beautiful mess.
- 记得在
@PreDestroy里关掉线程池,不然容器停不下来。
随手写点噪音:日志、 情绪、随机表情 😱🤖🙈
哎呀,我的咖啡者阝凉了还要调这个线程池大小!
⚡️紧急提醒⚡️: 别把 .exe 的路径写死,否则搬家时哭到天亮。
| # | 产品名称 | 支持异步方式 | 是否自带 Latch 支持? | 用户满意度 |
|---|---|---|---|---|
| 1 | AIO ExecMaster Pro | @Async + ExecutorService ProcessBuilder 原生支持 | No | 4.6/5 ★★★★☆ |
| 2 | Boom! Async Runner Lite | Simplified @Async 注解 | Yes | 4.2/5 ★★★☆☆ |
| 3 | CleverShell Integrator | Scripting + ThreadPoolExecutor | No | 4.8/5 ★★★★★ |
| 4 | DynaTask Scheduler X | ScheduledExecutor + CompletableFuture | Yes 4.4/5 ★★★★✩ | |
| ※以上排名纯属个人感受, 实际请自行测试 🚀🚀🚀. | ||||
实战演练:从零到有,一个完整示例
@SpringBootApplication
@EnableAsync
public class DemoApplication {
public static void main {
SpringApplication.run;
}
}
----------------------------------------------------
@Component
class StartupTask implements ApplicationRunner {
private final ExecutorService pool = Executors.newCachedThreadPool;
private final CountDownLatch latch = new CountDownLatch;
@Override
public void run throws Exception {
pool.submit;
latch.await; // 等待外部程序跑完再继续...
System.out.println;
doBusiness;
shutdown;
}
@Async
public void runExternal {
try {
Process p = new ProcessBuilder.inheritIO.start;
p.waitFor;
System.out.println;
} catch {
e.printStackTrace;
} finally {
latch.countDown; // 必须放这里否则可嫩卡死!
}
}
private void doBusiness { System.out.println; }
private void shutdown { pool.shutdownNow; }
}
坑点大集合🕳️🕳️🕳️:
- Pitfall 1:忘记在
@Async方法里捕获异常,导致整个线程池崩溃。 - Pitfall 2:使用单例 Service 时 把共享的 CountDownLatch 当成全局变量,一次调用成功后后面的调用永远卡住。
- Pitfall 3:ProcessBuilder 默认不继承父进程的 I/O, 你会堪不到仁和日志,只嫩自己读
.getInputStream. - Pitfall 4:SpringBoot 默认关闭钩子会等所you非守护线程结束,如guo你的线程池用了默认非守护线程,会导致容器停不下来。
- Pitfall 5:如guo你的 .exe 是 Windows GUI 程序, 它会弹出窗口阻塞控制台输出,让人抓狂。
- Pitfall 6:别忘了给外部娱乐加上施行权限,否则 Linux 环境下报 Permission denied。
- Pitfall 7:大量并发调用外部进程时 会出现 “Too many open files” 错误,需要调高系统文件句柄限制。
- Pitfall 8:记得给 Process 设置超时否则某些卡死的 exe 会让你的服务一直挂起。
- Pitfall 9:不要把业务代码直接写在 finally 块里那样即使外部进程失败也会继续施行后面的业务——除非你真的想这么干。
- Pitfall 10:如guo你用了 Docker 容器, 要确保容器内有对应的 exe 或娱乐,否则启动瞬间报错。

