三個關鍵時間點

Backend 應用的生命週期有三個開發者最在乎的時間點:

1. 啟動時(Application startup):初始化資料庫連線、暖機 cache、載入設定——必須在開始接受請求之前完成。

2. 請求前後(Per-request hooks):在 handler 執行前做 auth / log / validate,在 handler 執行後做 response 格式化 / cleanup。

3. 關閉時(Application shutdown):收到 SIGTERM,不再接新請求,等 in-flight request 完成,依序關閉 DB pool / Redis / Queue,乾淨退出。

這三個時間點,各框架給的控制程度不一樣。


啟動 Hook

NestJS

NestJS 有兩個啟動 hook:

@Injectable()
export class DatabaseService implements OnModuleInit, OnApplicationBootstrap {
  async onModuleInit() {
    // Module 初始化時:建立 DB 連線
    await this.db.connect();
  }
 
  async onApplicationBootstrap() {
    // 所有 module 都初始化完、server 開始監聽之前
    await this.cacheWarming();
  }
}

onModuleInit:每個 module 初始化時個別觸發。onApplicationBootstrap:所有 module 都 ready 後,server 開始 listen 之前觸發。適合做「需要所有依賴都就緒才能做的事」(例如 cache warming 需要 DB 和 Redis 都好了才能跑)。

FastAPI

from contextlib import asynccontextmanager
 
@asynccontextmanager
async def lifespan(app: FastAPI):
    # 啟動時
    await database.connect()
    redis_client = await create_redis_pool()
    yield  # ← application 跑在這裡
    # 關閉時
    await database.disconnect()
    await redis_client.close()
 
app = FastAPI(lifespan=lifespan)

FastAPI 的 lifespan context manager 同時管啟動和關閉,yield 前是啟動邏輯,yield 後是關閉邏輯。這個設計讓啟動和關閉的對稱關係非常直觀。

Spring Boot

@Component
public class AppStartupRunner implements ApplicationRunner {
  @Autowired
  private CacheService cacheService;
 
  @Override
  public void run(ApplicationArguments args) throws Exception {
    // ApplicationContext 完全初始化後執行
    cacheService.warmUp();
  }
}

或用 @PostConstruct(在 Bean 建立、依賴注入完成後觸發):

@Service
public class UserService {
  @PostConstruct
  public void init() {
    // 這個 service 準備好時的初始化邏輯
  }
}

Express(沒有內建 hook)

Express 沒有啟動 hook。你要自己設計啟動序列:

// 這就是為什麼 proto 有 bootstrap/ 層
const bootstrap = async (app) => {
  configureMiddleware(app);
  configureRoutes(app);
  await initializeDatabase(); // ← 你自己控制順序
  await initializeRedis();
  initializeWorkers();
};

詳見 Express Bootstrap 模式


請求級別 Hook(Per-request)

Spring:HandlerInterceptor

@Component
public class RequestLoggingInterceptor implements HandlerInterceptor {
  @Override
  public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
    req.setAttribute("startTime", System.currentTimeMillis());
    log.info("→ {} {}", req.getMethod(), req.getRequestURI());
    return true; // false = 中止請求
  }
 
  @Override
  public void afterCompletion(HttpServletRequest req, HttpServletResponse res,
                              Object handler, Exception ex) {
    long duration = System.currentTimeMillis() - (Long) req.getAttribute("startTime");
    log.info("← {} {} {}ms", req.getMethod(), req.getRequestURI(), duration);
  }
}

Spring 的 interceptor 有明確的 preHandle / postHandle / afterCompletion 三個時間點,非常結構化。

NestJS:Interceptor

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const start = Date.now();
    const req = context.switchToHttp().getRequest();
    console.log(`→ ${req.method} ${req.url}`);
 
    return next.handle().pipe(
      tap(() => {
        const duration = Date.now() - start;
        console.log(`← ${req.method} ${req.url} ${duration}ms`);
      }),
    );
  }
}

NestJS 用 RxJS Observable 實作 onion 模型,next.handle() 前是 pre,pipe(tap(...)) 是 post。

Express

Express 沒有「請求級別 hook」這個概念——middleware 就是 hook:

// 這就是 "before request" hook
app.use((req, res, next) => {
  req.startTime = Date.now();
  next();
});
 
// 這就是 "after request" hook(用 event)
app.use((req, res, next) => {
  res.on('finish', () => {
    console.log(`← ${req.method} ${req.url} ${Date.now() - req.startTime}ms`);
  });
  next();
});

關閉 Hook

這個是最重要的,直接影響 K8s 滾動部署是否有 downtime。

NestJS

@Injectable()
export class DatabaseService implements OnModuleDestroy {
  async onModuleDestroy() {
    await this.db.close();
  }
}
 
// main.ts
app.enableShutdownHooks(); // ← 開啟 SIGTERM / SIGINT 監聽
await app.listen(3000);

NestJS 的 enableShutdownHooks() 讓它自動監聽 SIGTERM/SIGINT,按 module 依賴的反向順序呼叫所有的 onModuleDestroy()

FastAPI

lifespan 的 yield 後就是關閉邏輯(前面已展示)。非常直觀。

Spring Boot

@Component
public class GracefulShutdown implements DisposableBean {
  @Override
  public void destroy() throws Exception {
    // Bean 銷毀時(application shutdown)
    log.info("Closing database connections...");
  }
}

或用 @PreDestroy(和 @PostConstruct 對稱)。

Spring Boot 2.3+ 原生支援 graceful shutdown:

# application.yml
server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

Express(手動)

process.on('SIGTERM', async () => {
  server.close(async () => {
    await sequelize.close();
    await redisClient.quit();
    await queue.close();
    process.exit(0);
  });
 
  setTimeout(() => process.exit(1), 25000); // 超時強制退出
});

啟動協定:node / pm2 / gunicorn / uvicorn

框架提供了 lifecycle hook,但實際上「誰啟動你的應用、誰管 process」也影響生命週期行為。

Node.js 直接啟動(node dist/bin/www.js

  • 單一 process,沒有 process manager
  • SIGTERM 觸發時,你的 process.on('SIGTERM', ...) handler 負責清理
  • K8s Pod 直接 kill 這個 process;容器退出碼就是 process.exit(code) 的值
  • 適合:Docker / K8s 環境,container orchestration 本身管 process lifecycle

PM2(pm2 start dist/bin/www.js

  • Process manager:監控 process,crash 自動重啟
  • 支援 cluster mode(多個 Node.js process 跑在同一台機器),利用多核
  • PM2 在 K8s 裡是多餘的——K8s 已經管 Pod 的重啟;PM2 + K8s 雙層管理反而造成混亂
  • 適合:VM / bare metal 環境,沒有 orchestration 層的場景

Gunicorn(Python WSGI)

gunicorn app:app --workers 4 --bind 0.0.0.0:8000
  • Multi-process WSGI server:fork 多個 worker process,每個跑一份 Django/Flask 應用
  • --workers 4 代表 4 個 worker,最多同時處理 4 個並發請求(同步 WSGI 的限制)
  • 收到 SIGTERM:停止接新請求,等 in-flight request 完成,然後關閉 worker
  • K8s 裡適合直接用,不需要 pm2 這類外層管理器

Uvicorn(Python ASGI)

uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
  • ASGI server:跑 FastAPI / Starlette 的標準方式
  • --workers 4 效果類似 Gunicorn,但底層是 event loop(非同步)而非 thread-per-worker
  • Gunicorn + Uvicorn worker(-k uvicorn.workers.UvicornWorker)是 production 常見組合,讓 Gunicorn 管 process lifecycle,Uvicorn 跑 event loop
  • K8s 裡直接 uvicorn ... --workers N 通常就夠了
Node.js/K8s:node 直接啟動,K8s 管重啟
Python/K8s:uvicorn 直接啟動(或 gunicorn + uvicorn worker),K8s 管重啟
Node.js/VM:pm2 管 process,multi-core cluster 跑多個 worker
Python/VM:gunicorn 管 process,multi-worker 利用多核

生命週期控制程度比較

啟動 hook請求級 hook關閉 hook需要自己實作
Spring Boot✅ 完整✅ Interceptor✅ 原生支援極少
NestJS✅ onModuleInit✅ Interceptor✅ enableShutdownHooks
FastAPI✅ lifespan需要 middleware✅ lifespan yield 後中等
Django✅ AppConfig.ready✅ Middleware需要自己加中等
Express❌ 無middleware 模擬❌ 無全部
Gin❌ 無middleware 模擬❌ 無全部

Express 和 Gin 的「沒有內建」不是缺點——它的意思是這些框架相信你知道自己在做什麼,而且你可以自由選擇實作方式。代價是你必須自己把這些東西設計好,不能依賴框架。