custom annotations trong java

- Published on
- /10 mins read/
Trong hệ sinh thái Java và Spring Boot hiện đại, Annotations (Chú thích) xuất hiện ở khắp mọi nơi: từ @Entity, @Transactional, @RestController đến @PreAuthorize. Chúng biến những đoạn code cấu hình dài dòng thành những dòng chú thích thanh lịch, trừu tượng hóa sự phức tạp của hạ tầng.
Tuy nhiên, với nhiều kỹ sư phần mềm, Annotation vẫn là một "hộp đen ma thuật" (black box). Nhiều người lầm tưởng rằng Annotation bản thân nó chứa mã thực thi, hoặc lạm dụng reflection thô sơ (Field.setAccessible(true)) dẫn đến sụt giảm nghiêm trọng hiệu năng hệ thống.
Bài viết này sẽ phân tích cơ chế hoạt động nội tại của Java Annotation từ tầng bytecode JVM, giải mã cơ chế Spring Meta-Annotations (@AliasFor), và từng bước thiết kế một Custom Annotation chuẩn production: Distributed Rate Limiter sử dụng Spring AOP kết hợp Spring Expression Language (SpEL) và Redis.
# bản chất Java annotation dưới góc nhìn Bytecode JVM
Một trong những chân lý quan trọng nhất: Annotation hoàn toàn KHÔNG chứa mã thực thi logic. Annotation đơn thuần là các Metadata (siêu dữ liệu) được đính kèm vào mã nguồn để các công cụ bên ngoài (Compiler, Bytecode Manipulator, hoặc Runtime Reflection Engine) đọc và hành xử tương ứng.
# điều gì thực sự xảy ra trong JVM khi gọi clazz.getannotation()?
Khi bạn khai báo một annotation:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimited {
int rpm() default 60;
String key();
}Trình biên dịch javac sẽ biên dịch interface này thành một interface đặc biệt mở rộng từ java.lang.annotation.Annotation. Trong file .class, javac tạo ra một thuộc tính bytecode có tên là RuntimeVisibleAnnotations.
Khi ứng dụng chạy và bạn gọi method.getAnnotation(RateLimited.class):
- JVM không khởi tạo một class thông thường. Thay vào đó, JVM sinh động một Java Dynamic Proxy (
java.lang.reflect.Proxy). - Proxy này bọc một
InvocationHandlernội bộ của JVM (thường làsun.reflect.annotation.AnnotationInvocationHandler). - Khi bạn gọi
annotation.rpm(), proxy sẽ tra cứu giá trị trong bảng hằng số (Constant Pool) hoặc bảng map metadata đã được nạp vào Metaspace.
NOTE
Do cơ chế Dynamic Proxy và Reflection tra cứu Metadata, việc gọi reflection trong hot-path (vòng lặp hàng triệu lần mỗi giây) sẽ gây ra overhead đáng kể. Do đó, các enterprise framework luôn cache kết quả scan annotation ngay trong giai đoạn khởi động (Bootstrap phase).
# phân tầng retentionpolicy: khi nào dùng gì?
Hiểu đúng vòng đời của Annotation giúp bạn tối ưu hóa hiệu năng và thiết kế kiến trúc chuẩn mực:
| RetentionPolicy | Thời điểm phân tích | Cơ chế hoạt động | Use Case thực tế | Chi phí Runtime |
|---|---|---|---|---|
SOURCE | Compile-time | Trình biên dịch đọc và bỏ qua khi xuất bytecode; hoặc can thiệp AST qua JSR 269 Annotation Processor. | @Override, Lombok (@Getter, @Builder), MapStruct (@Mapper). | 0% (Zero) |
CLASS | Post-compile / Bytecode Enhancement | Nằm trong .class file nhưng không nạp vào RAM lúc runtime. Dùng cho công cụ can thiệp bytecode offline. | Byte Buddy, Jacoco Code Coverage, GraalVM native image build hints. | 0% |
RUNTIME | Running JVM | Nạp vào Metaspace, đọc thông qua Reflection hoặc Spring AOP / CDI Interceptors. | Spring @Transactional, @RestController, Jackson @JsonProperty. | Có chi phí tra cứu Reflection ban đầu |
# Spring meta-annotations & cơ chế @aliasfor
Trong Spring Framework, bạn thường thấy các annotation kỳ diệu như @RestController thực chất được cấu thành từ @Controller và @ResponseBody. Đây chính là mô hình Meta-Annotations (Annotation lồng Annotation).
Java thuần túy không hỗ trợ kế thừa giữa các @interface. Spring giải quyết hạn chế này bằng công cụ AnnotatedElementUtils và annotation @AliasFor:
# ví dụ triển khai @aliasfor
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@PostMapping // Meta-annotation của Spring
public @interface AuditedPostMapping {
// Tạo alias cho thuộc tính path của @PostMapping
@AliasFor(annotation = PostMapping.class, attribute = "path")
String[] path() default {};
// Thuộc tính riêng của audit
String auditAction();
boolean maskSensitiveData() default true;
}Nhờ @AliasFor, Spring DispatcherServlet vẫn nhận diện method này là một HTTP POST handler chuẩn mực, đồng thời module Audit AOP vẫn có thể trích xuất auditAction mà không cần viết lặp lại các khai báo.
# xây dựng custom annotation chuẩn enterprise: distributed rate limiter
Hãy cùng xây dựng một tính năng thực chiến: Một Annotation @DistributedRateLimit có khả năng:
- Đặt trên bất kỳ Service method hoặc Controller nào.
- Trích xuất tham số động trong runtime (ví dụ:
userId,ipAddress) bằng Spring Expression Language (SpEL). - Chặn request vượt ngưỡng dựa trên thuật toán Sliding Window Token Bucket thông qua Redis.
- Không làm ô nhiễm logic nghiệp vụ của ứng dụng (Separation of Concerns).
# khai báo custom annotation
package com.company.common.ratelimit;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DistributedRateLimit {
/**
* SpEL Expression để xác định định danh rate limit.
* Ví dụ: "#userId", "#request.customerId", hoặc "#ip"
*/
String key();
/**
* Số lượng request tối đa trong cửa sổ thời gian.
*/
int limit() default 100;
/**
* Độ dài của cửa sổ thời gian.
*/
long duration() default 60;
/**
* Đơn vị thời gian (mặc định: SECONDS).
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* Thông báo lỗi khi bị chặn.
*/
String message() default "Hệ thống đang bận. Vui lòng thử lại sau!";
}# thiết kế aspect xử lý bằng Spring AOP & SpEL parser
Điểm mấu chốt của một Senior Engineer là xử lý SpEL động để lấy tham số thực tế truyền vào hàm:
package com.company.common.ratelimit;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.time.Duration;
@Aspect
@Component
public class DistributedRateLimitAspect {
private static final Logger log = LoggerFactory.getLogger(DistributedRateLimitAspect.class);
private final StringRedisTemplate redisTemplate;
private final ExpressionParser spelParser = new SpelExpressionParser();
private final ParameterNameDiscoverer paramDiscoverer = new DefaultParameterNameDiscoverer();
public DistributedRateLimitAspect(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Around("@annotation(rateLimit)")
public Object enforceRateLimit(ProceedingJoinPoint joinPoint, DistributedRateLimit rateLimit) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 1. Phân giải SpEL để trích xuất Dynamic Key từ method parameters
String evaluatedKey = evaluateSpelExpression(rateLimit.key(), method, joinPoint.getArgs(), joinPoint.getTarget());
String redisKey = String.format("ratelimit:%s:%s", method.getName(), evaluatedKey);
// 2. Kiểm tra token với Redis (Atomic Increment & Expire)
long durationSeconds = rateLimit.timeUnit().toSeconds(rateLimit.duration());
Long currentRequests = redisTemplate.opsForValue().increment(redisKey);
if (currentRequests != null && currentRequests == 1) {
// Đặt TTL cho key ngay ở request đầu tiên
redisTemplate.expire(redisKey, Duration.ofSeconds(durationSeconds));
}
if (currentRequests != null && currentRequests > rateLimit.limit()) {
log.warn("Rate limit breached for key: {} (count: {}/{})", redisKey, currentRequests, rateLimit.limit());
throw new RateLimitExceededException(rateLimit.message());
}
// 3. Tiến hành gọi method thực tế
return joinPoint.proceed();
}
private String evaluateSpelExpression(String expressionStr, Method method, Object[] args, Object target) {
if (!expressionStr.startsWith("#")) {
return expressionStr; // Static key thuần túy
}
MethodBasedEvaluationContext context = new MethodBasedEvaluationContext(
target, method, args, this.paramDiscoverer
);
Object val = spelParser.parseExpression(expressionStr).getValue(context);
return val != null ? val.toString() : "anonymous";
}
}# ứng dụng annotation vào business service
@Service
public class PaymentProcessingService {
/**
* Giới hạn mỗi User chỉ được thực hiện tối đa 5 giao dịch thanh toán trong vòng 60 giây.
* Khóa rate-limit được trích xuất trực tiếp từ trường customerId của object request!
*/
@DistributedRateLimit(
key = "#request.customerId",
limit = 5,
duration = 60,
timeUnit = TimeUnit.SECONDS,
message = "Bạn đã thực hiện quá nhiều giao dịch. Vui lòng chờ 1 phút!"
)
public TransactionReceipt processPayment(PaymentRequest request) {
// Business logic an toàn tuyệt đối, không cần bận tâm về rate-limiting code
return executePayment(request);
}
}# những "điểm gãy" kinh điển cần tránh khi dùng custom annotation
# bẫy tự gọi nội bộ (self-invocation trap)
Spring AOP hoạt động dựa trên cơ chế Dynamic Proxy. Khi một method bên ngoài gọi bean thông qua Proxy, Aspect mới được kích hoạt. Nếu method A gọi method B cùng nằm trong cùng một class:
@Service
public class OrderService {
public void processAll() {
// GỌI NỘI BỘ (this.executeSingle): Proxy BỊ BYPASS HOÀN TOÀN!
// Annotation @DistributedRateLimit trên executeSingle() sẽ KHÔNG CHẠY!
this.executeSingle();
}
@DistributedRateLimit(key = "'fixed'", limit = 2)
public void executeSingle() {
// ...
}
}Giải pháp: Tách executeSingle() sang một Service riêng biệt, hoặc inject chính proxy của service đó thông qua ObjectProvider<OrderService>.
# chi phí reflection cpu & memory leaks
Tránh dùng clazz.getDeclaredFields() và field.setAccessible(true) lặp đi lặp lại trong hot-path của ứng dụng. Mỗi lần gọi getDeclaredFields():
- JVM phải clone mảng
Field[]để bảo vệ tính bất biến. - Gây rác bộ nhớ (GC Allocation Churn) và tiêu tốn CPU cycles.
- Giải pháp: Sử dụng
org.springframework.util.ReflectionUtilskết hợp bộ nhớ cacheConcurrentHashMap<Method, Metadata>.
# tổng kết
| Tiêu chí | Custom Annotation ngây thơ | Custom Annotation chuẩn production |
|---|---|---|
| Cơ chế xử lý | Reflection quét thủ công tại runtime qua vòng lặp. | Spring AOP Aspect chặn trước/sau method execution. |
| Quản lý tham số | Fix cứng tên field hoặc tham số tĩnh. | Tích hợp SpEL để phân giải tham số động tại runtime. |
| Xử lý Proxy | Dễ gặp bẫy Self-Invocation làm mất tác dụng. | Thiết kế tuân thủ ranh giới Bean và kiểm soát injection. |
| Khả năng mở rộng | Gắn chặt logic vào một class xử lý. | Phân tách rành mạch (Separation of Concerns): Domain thuần khiết, hạ tầng nằm ở Aspect. |
Java Custom Annotation kết hợp cùng Spring AOP là một vũ khí kiến trúc tối thượng. Khi được làm chủ đúng cách, nó giúp giữ cho mã nguồn nghiệp vụ của bạn luôn tinh gọn, sáng sủa, đồng thời tập trung toàn bộ các bài toán kỹ thuật phức tạp (Security, Audit, Rate Limiting, Metrics) về một điểm kiểm soát duy nhất.
Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!
Happy coding 😎 👍🏻 🚀 🔥.
On this page
- # bản chất Java annotation dưới góc nhìn Bytecode JVM
- # điều gì thực sự xảy ra trong JVM khi gọi clazz.getannotation()?
- # phân tầng retentionpolicy: khi nào dùng gì?
- # Spring meta-annotations & cơ chế @aliasfor
- # ví dụ triển khai @aliasfor
- # xây dựng custom annotation chuẩn enterprise: distributed rate limiter
- # khai báo custom annotation
- # thiết kế aspect xử lý bằng Spring AOP & SpEL parser
- # ứng dụng annotation vào business service
- # những "điểm gãy" kinh điển cần tránh khi dùng custom annotation
- # bẫy tự gọi nội bộ (self-invocation trap)
- # chi phí reflection cpu & memory leaks
- # tổng kết