三個關鍵時間點
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();
};請求級別 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: 30sExpress(手動)
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 的「沒有內建」不是缺點——它的意思是這些框架相信你知道自己在做什麼,而且你可以自由選擇實作方式。代價是你必須自己把這些東西設計好,不能依賴框架。
