三個關鍵時間點

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); // 超時強制退出
});

生命週期控制程度比較

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