Design patterns bên trong Spring
Tiếp đi: Spring dùng những design pattern nào bên trong?
Bài 1.16 tự viết pattern cho shop-backend. Bài này lật ngược lại: mỗi ngày bạn đã dùng các pattern đó qua Spring mà không để ý. Hiểu chúng giúp debug và trả lời câu phỏng vấn kinh điển "Spring dùng pattern gì?".
1. Bài toán: một request POST /orders đi qua những gì
POST /orders
→ Filter (log, auth) ← ?
→ DispatcherServlet → HandlerAdapter ← ?
→ OrderController (bean do ai tạo?) ← ?
→ OrderService (@Transactional) ← ?
→ JdbcTemplate.update(...) ← ?
→ publishEvent(OrderPlaced) ← ?
2. Vấn đề
Nếu coi Spring là "phép màu", bạn sẽ không hiểu vì sao @Transactional gọi trong cùng class không chạy, vì sao bean prototype inject vào singleton không ra object mới, hay filter chạy theo thứ tự nào.
3. Giải pháp: gắn tên pattern cho từng chỗ
| Pattern | Trong Spring | Bài chi tiết |
|---|---|---|
| Factory / IoC | BeanFactory, ApplicationContext, FactoryBean, @Bean method | IoC & DI |
| Singleton | Scope mặc định của bean | Bean scope |
| Proxy | AOP, @Transactional, @Async, @Cacheable | AOP & proxy |
| Template Method | JdbcTemplate, RestTemplate, TransactionTemplate | bên dưới |
| Observer | ApplicationEvent, @EventListener | Events |
| Strategy | Inject List<T>/Map<String,T>, PasswordEncoder, ResourceLoader | bên dưới |
| Adapter | HandlerAdapter | bên dưới |
| Chain of Responsibility | Servlet filter, Spring Security FilterChain, HandlerInterceptor | bên dưới |
4. Code từng bước
Bước 1: Factory, Spring là nhà máy tạo bean
@Configuration
class PaymentConfig {
@Bean // factory method
MomoClient momoClient(@Value("${momo.secret}") String secret) {
return new MomoClient(secret);
}
}
// Bạn không gọi new; ApplicationContext (một BeanFactory) tạo và inject.
// FactoryBean<T>: bean mà bản thân nó là nhà máy, ví dụ Spring Data tạo OrderRepository.
Bước 2: Template Method trong JdbcTemplate
List<Order> orders = jdbc.query(
"select id, status from orders where customer_id = ?",
(rs, i) -> new Order(rs.getString("id"), OrderStatus.valueOf(rs.getString("status"))),
customerId);
JdbcTemplate làm (khung cố định): Bạn chỉ viết (bước thay đổi):
lấy Connection SQL
tạo PreparedStatement, set param RowMapper (lambda)
execute, lặp ResultSet ──────────▶ map 1 dòng
đổi SQLException → DataAccessException
đóng ResultSet, Statement, Connection
Khác Template Method kinh điển một chút: phần thay đổi truyền vào bằng callback (lambda) thay vì subclass. RestTemplate, TransactionTemplate, KafkaTemplate cùng ý tưởng.
Bước 3: Strategy bằng inject nhiều bean
@Service
class NotificationService {
private final Map<String, Notifier> notifiers; // key = bean name: "email", "sms"
NotificationService(Map<String, Notifier> notifiers) { this.notifiers = notifiers; }
void notify(Customer c, String msg) { notifiers.get(c.preferredChannel()).send(c, msg); }
}
Bước 4: Chain of Responsibility bằng filter
@Component
@Order(1)
class CorrelationIdFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws ServletException, IOException {
MDC.put("cid", Optional.ofNullable(req.getHeader("X-Correlation-Id")).orElse(UUID.randomUUID().toString()));
try { chain.doFilter(req, res); } // chuyển cho mắt xích kế tiếp
finally { MDC.remove("cid"); }
}
}
Mỗi filter tự quyết: xử lý rồi chuyển tiếp, hoặc dừng chuỗi (filter auth trả 401 và không gọi chain.doFilter).
5. Cách hoạt động
Request
│ Chain of Responsibility
├─▶ CorrelationIdFilter ─▶ SecurityFilterChain ─▶ ...
│
▼ DispatcherServlet
HandlerMapping tìm handler (method @GetMapping)
HandlerAdapter (Adapter): RequestMappingHandlerAdapter biết gọi method có
@RequestBody, @PathVariable... DispatcherServlet chỉ biết interface HandlerAdapter
│
▼ OrderService$$SpringCGLIB (Proxy)
begin tx → OrderService thật (bean singleton tạo bởi BeanFactory)
→ JdbcTemplate (Template)
→ publishEvent (Observer) → @EventListener ...
commit/rollback
6. So sánh: tự viết và Spring làm sẵn
| Bài 1.16 tự viết | Spring có sẵn |
|---|---|
| Singleton enum / getInstance() | Singleton scope + DI |
PaymentGatewayFactory | Inject Map<String, PaymentGateway> |
| Decorator retry thủ công | @Retryable (proxy) |
| Observer tự quản lý list listener | ApplicationEventPublisher |
| try/finally đóng Connection | JdbcTemplate |
7. Khi nào nên dùng / không nên dùng
- Nên: tận dụng cái Spring có trước khi tự viết pattern.
- Không nên: tự tạo proxy hay Singleton song song với container, sẽ có 2 cách quản lý object.
- Nhớ giới hạn của proxy: gọi nội bộ (
this.method()) không qua proxy.
HandlerAdapter "adapt" cái gì sang cái gì?Mình hiểu Adapter khi bọc SDK cũ. Nhưng HandlerAdapter trong Spring MVC thì adapt cái gì? Controller có phải SDK cũ đâu.
Đúng là không có SDK cũ, nhưng vấn đề giống hệt: nhiều kiểu handler có hình dạng khác nhau, còn DispatcherServlet muốn gọi chúng bằng một cách duy nhất.
Kiểu handler Adapter tương ứng
@Controller method (@GetMapping) → RequestMappingHandlerAdapter
class implements Controller (cũ) → SimpleControllerHandlerAdapter
HttpRequestHandler → HttpRequestHandlerAdapter
HandlerFunction (RouterFunction) → HandlerFunctionAdapter
DispatcherServlet:
for (HandlerAdapter a : adapters)
if (a.supports(handler)) return a.handle(req, res, handler); // 1 interface chung
Nhờ vậy Spring thêm kiểu handler mới (functional endpoint) mà không sửa DispatcherServlet. Adapter ở đây kèm luôn việc resolve tham số (@RequestBody → đối tượng) và ghi kết quả (đối tượng → JSON).
Filter với HandlerInterceptor đều chạy trước controller. Mình muốn log thời gian xử lý đơn hàng thì đặt ở đâu?
Cả hai đều là Chain of Responsibility, chỉ khác tầng:
| Filter | HandlerInterceptor | |
|---|---|---|
| Thuộc | Servlet container | Spring MVC |
| Chạy | Trước DispatcherServlet | Sau khi đã biết handler |
| Biết controller nào? | Không | Có (HandlerMethod) |
| Đổi request/response stream | Được (wrap) | Không |
| Ví dụ | Correlation id, auth, CORS, nén | Log theo controller, kiểm tra quyền theo annotation |
Log thời gian cho mọi request: filter. Log kèm tên method controller hoặc đọc annotation trên method: interceptor. Muốn đo một method service: AOP @Around.
Phỏng vấn hỏi "Spring sử dụng những design pattern nào?" thì trả lời sao?
Câu 1: Spring dùng design pattern nào?
🇻🇳 Tiếng Việt:
Factory qua
BeanFactory/ApplicationContexttạo bean; Singleton là scope mặc định; Proxy cho AOP như@Transactional,@Async; Template Method trongJdbcTemplate,RestTemplate; Observer quaApplicationEvent; Strategy khi inject list bean cùng interface; Adapter trongHandlerAdapter; Chain of Responsibility trong filter chain của Spring Security.
🇬🇧 English:
Factory through
BeanFactory/ApplicationContext; Singleton as the default scope; Proxy for AOP such as@Transactionaland@Async; Template Method inJdbcTemplateandRestTemplate; Observer viaApplicationEvent; Strategy when injecting a list of beans of one interface; Adapter inHandlerAdapter; and Chain of Responsibility in the Spring Security filter chain.
Câu 2: Singleton bean của Spring khác Singleton pattern thế nào?
🇻🇳 Tiếng Việt:
Singleton pattern đảm bảo một instance mỗi ClassLoader, tự quản lý qua static. Singleton bean là một instance mỗi container cho một bean definition; vẫn có thể có nhiều bean cùng class với tên khác, và được inject nên dễ test.
🇬🇧 English:
The Singleton pattern guarantees one instance per ClassLoader and manages it statically. A Spring singleton bean is one instance per container per bean definition; you can still have several beans of the same class under different names, and they are injected, so they're easy to test.
Cách nhớ ngắn gọn
- Tạo bean: Factory + Singleton.
- Annotation "thêm hành vi": Proxy.
- *Template: khung cố định, bạn đưa callback.
- Event: Observer. List bean: Strategy.
- Web: filter = Chain, HandlerAdapter = Adapter.
Tự kiểm tra
JdbcTemplate lo phần nào, bạn lo phần nào?
Nó lo connection, statement, lặp ResultSet, đổi exception, đóng resource. Bạn đưa SQL, tham số và RowMapper.
Vì sao @Transactional là Proxy chứ không phải Decorator?
Framework tự tạo và đặt thay cho bean thật để kiểm soát truy cập (mở/đóng transaction), client không tự xếp lớp.
Filter auth muốn dừng request thì làm gì?
Ghi response (401) và không gọi chain.doFilter.
Inject Map<String, Notifier> thì key là gì?
Tên bean của từng implementation.