Custom annotation và Spring AOP
Annotation chỉ là metadata. Muốn nó tạo behavior, phải có code đọc metadata bằng compiler, reflection, interceptor, BeanPostProcessor hoặc Spring AOP proxy.
1. AOP bổ sung OOP như thế nào?
Aspect-Oriented Programming tách concern cắt ngang nhiều module như transaction, security, audit, metrics và tracing khỏi business code. OOP chia hệ thống theo object và responsibility; AOP mô tả behavior áp tại nhiều join point. Hai cách bổ sung nhau, nhưng AOP không thay encapsulation hoặc domain design.
| Thuật ngữ | Ý nghĩa |
|---|---|
| Aspect | Module chứa cross-cutting concern. |
| Join point | Điểm có thể áp behavior; Spring AOP chủ yếu là method execution. |
| Pointcut | Biểu thức chọn join points. |
| Advice | Code chạy before, after hoặc around join point. |
| Weaving | Gắn aspect vào target; Spring thường dùng runtime proxy, AspectJ có compile/load-time weaving. |
2. Tạo annotation đúng retention và target
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuditedAction {
String value();
boolean includeResult() default false;
}
SOURCEchỉ tồn tại lúc compile;CLASSnằm trong bytecode nhưng reflection runtime không bắt buộc thấy;RUNTIMEcho reflection/AOP đọc được.@Targetgiới hạn nơi sử dụng;@Inheritedchỉ áp cho class annotation qua inheritance, không tự áp cho method hoặc interface.- Annotation element chỉ hỗ trợ primitive, String, Class, enum, annotation hoặc array tương ứng; không chứa mutable object.
3. Aspect intercept annotation
@Aspect
@Component
class AuditAspect {
private final AuditSink sink;
@Around("@annotation(audited)")
Object audit(ProceedingJoinPoint pjp, AuditedAction audited) throws Throwable {
long started = System.nanoTime();
try {
Object result = pjp.proceed(); // gọi đúng một lần
sink.success(audited.value(), elapsed(started));
return result;
} catch (Throwable error) {
sink.failure(audited.value(), error, elapsed(started));
throw error;
}
}
}
@Around mạnh nhất nhưng dễ sai nhất: quên proceed(), gọi hai lần, đổi arguments/result hoặc nuốt exception. Audit sink không được log password, token hay PII và không nên làm remote I/O chậm trên request thread.
4. Pointcut và proxy boundary
@AuditedAction("TRANSFER_APPROVE")
public Transfer approve(UUID id) { ... }
public void batchApprove(List<UUID> ids) {
ids.forEach(this::approve); // self-invocation: aspect không chạy
}
Spring AOP chỉ intercept call đi qua Spring-managed proxy. Public call từ bean khác thường được intercept; self-invocation, private/final method và object tạo bằng new không tạo boundary như mong đợi. Tách annotated operation sang collaborator thay vì self-injection.
5. Meta-annotation và composed annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Transactional
@AuditedAction("MONEY_MOVEMENT")
public @interface MoneyTransaction {
@AliasFor(annotation = Transactional.class, attribute = "readOnly")
boolean readOnly() default false;
}
Composed annotation tạo vocabulary của application nhưng dễ giấu behavior. Chỉ dùng khi semantics ổn định, tên thể hiện intent và team hiểu transaction/audit ordering. Khi tìm annotation trên implementation hoặc proxy, dùng Spring utilities thay vì reflection ngây thơ.
6. Ordering giữa aspects
security → retry → transaction → target → transaction completion → metrics
Thứ tự thực phụ thuộc @Order hoặc Ordered. Retry ngoài transaction tạo transaction mới mỗi attempt; audit success nằm trong transaction có thể được ghi trước khi commit thất bại. Nếu cần sự kiện “đã commit”, dùng transaction synchronization hoặc Outbox. Viết integration test cho call order, exception và rollback.
7. Khi nào không nên dùng AOP?
| Dùng tốt | Không nên giấu trong aspect |
|---|---|
| Metrics/timing, authorization policy, transaction, tracing, stable audit envelope. | Business state transition, orchestration, compensation, pricing và flow cần đọc thấy trực tiếp. |
Nếu annotation tạo side effect bất ngờ, đổi business result hoặc buộc người đọc nhớ nhiều ordering ngầm, explicit decorator hoặc service thường dễ hiểu và test hơn.
8. Custom validation không nhất thiết dùng AOP
@Constraint(validatedBy = ValidMoneyValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidMoney {
String message() default "invalid money";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Custom Bean Validation annotation được Jakarta Validation provider xử lý, không phải Spring AOP. Annotation cũng có thể do compiler, plugin hoặc framework khác xử lý; luôn hỏi “ai đọc annotation và ở phase nào?”.
9. Checklist phỏng vấn và production
- Giải thích annotation là metadata, không tự chạy code.
- Chọn đúng Target, Retention và processor runtime hoặc compile-time.
- Hiểu proxy, self-invocation, final/private limitation.
@Aroundgọiproceed()đúng một lần và rethrow đúng semantics.- Test ordering với transaction, retry và security.
- Redact dữ liệu nhạy cảm, giới hạn latency và metric cardinality.
- Biết khi nào dùng interceptor, decorator hoặc validation thay AOP.